@yemi33/minions 0.1.1959 → 0.1.1961

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/docs/skills.md ADDED
@@ -0,0 +1,43 @@
1
+ # Skill Block Format
2
+
3
+ Agents emit a fenced `skill` block when they discover a durable, reusable workflow that future agents are likely to need. See [`playbooks/shared-rules.md`](../playbooks/shared-rules.md) for the eligibility threshold and the "zero skills is the default" rule.
4
+
5
+ The engine extracts valid blocks from agent stdout and from inbox findings (`notes/inbox/*.md`) and writes them into the selected runtime's native personal skills directory (e.g. `~/.claude/skills/<name>/SKILL.md` for Claude, the equivalent personal skills directory for Copilot). Project-scoped skills are landed via PR into the matching project's playbook tree.
6
+
7
+ ## Block format
8
+
9
+ ````
10
+ ```skill
11
+ ---
12
+ name: short-descriptive-name
13
+ description: One-line description of when to trigger this skill
14
+ scope: minions
15
+ ---
16
+
17
+ # Skill Title
18
+
19
+ ## Steps
20
+ 1. ...
21
+ 2. ...
22
+
23
+ ## Notes
24
+ ...
25
+ ```
26
+ ````
27
+
28
+ ### Frontmatter fields
29
+
30
+ - `name` (**required**) — kebab-case identifier; becomes the on-disk directory name.
31
+ - `description` (**required**) — one-line "when to use this" hint shown in skill listings.
32
+ - `scope` — `minions` (default; user-level skill, available in any runtime window) or `project` (repo-local skill, landed via PR into that project).
33
+ - `project` — required when `scope: project`; the project name the skill belongs to.
34
+ - `allowed-tools` (optional) — comma-separated tools the skill is permitted to call (e.g. `Bash, Read, Edit`).
35
+ - `trigger` (optional) — natural-language phrase describing when an agent should invoke the skill.
36
+
37
+ ### Body
38
+
39
+ Plain Markdown. Include the steps, commands, file paths, and notes another agent needs to follow the workflow without reading the original task context. Cite source files and PR numbers where useful — the body is what future agents actually read.
40
+
41
+ ## When not to emit a skill
42
+
43
+ See [`playbooks/shared-rules.md`](../playbooks/shared-rules.md) for the full threshold. In short: do not emit a skill for one-off bug fixes, isolated command output, obvious repo facts, or anything already covered by existing docs, playbooks, or skills.
@@ -2332,6 +2332,38 @@ function checkForLearnings(agentId, agentInfo, taskDesc) {
2332
2332
  log('info', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
2333
2333
  }
2334
2334
 
2335
+ // E2.a (W-mp7goxe4000p75f7): name-normalised dedup against the personal-scope
2336
+ // skill dir. Strips a trailing `-[a-z0-9]{4,8}` random-suffix on the candidate
2337
+ // and re-checks against existing stems; also does simple prefix-match.
2338
+ // Returns the matched existing stem, or null if no near-duplicate.
2339
+ // Read-only — never deletes or renames existing skills.
2340
+ function _findNearDuplicateSkill(personalSkillRoot, candidateName) {
2341
+ if (!personalSkillRoot || !candidateName) return null;
2342
+ let entries;
2343
+ try {
2344
+ entries = fs.readdirSync(personalSkillRoot, { withFileTypes: true })
2345
+ .filter(d => d.isDirectory())
2346
+ .map(d => d.name);
2347
+ } catch { return null; }
2348
+ if (entries.length === 0) return null;
2349
+
2350
+ // 1. Strip trailing `-[a-z0-9]{4,8}` random suffix and re-check.
2351
+ const stripped = candidateName.replace(/-[a-z0-9]{4,8}$/, '');
2352
+ if (stripped !== candidateName && entries.includes(stripped)) {
2353
+ return stripped;
2354
+ }
2355
+
2356
+ // 2. Prefix-match against existing stems. Either direction:
2357
+ // - candidate `foo-bar-x4y2` vs existing `foo-bar`
2358
+ // - candidate `foo` vs existing `foo-bar`
2359
+ for (const existing of entries) {
2360
+ if (existing === candidateName) continue;
2361
+ if (candidateName.startsWith(existing + '-')) return existing;
2362
+ if (existing.startsWith(candidateName + '-')) return existing;
2363
+ }
2364
+ return null;
2365
+ }
2366
+
2335
2367
  function skillWriteTargets(runtimeName, project = null) {
2336
2368
  try {
2337
2369
  const runtime = resolveRuntime(runtimeName || 'claude');
@@ -2408,22 +2440,42 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
2408
2440
  const personalSkillRoot = skillWriteTargets(effectiveRuntime).personal;
2409
2441
  const skillDir = path.join(personalSkillRoot, name.replace(/[^a-z0-9-]/g, '-'));
2410
2442
  const skillPath = path.join(skillDir, 'SKILL.md');
2411
- if (!fs.existsSync(skillPath)) {
2412
- // Native skill format: only name + description in frontmatter. The
2413
- // `Auto-extracted` marker is an HTML comment so the dashboard's
2414
- // autoGenerated detection picks it up without polluting the body
2415
- // an agent reads.
2416
- const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
2417
- const body = fmMatch[2] || '';
2418
- const marker = `<!-- Auto-extracted by ${agentName} on ${dateStamp()} -->`;
2419
- const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${marker}\n\n${body.trim()}\n`;
2420
- if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
2421
- shared.safeWrite(skillPath, ccContent);
2422
- try { require('./queries').invalidateSkillsCache(); } catch {}
2423
- log('info', `Extracted skill "${name}" from ${agentName} → ${skillPath}`);
2424
- } else {
2443
+ if (fs.existsSync(skillPath)) {
2425
2444
  log('info', `Skill "${name}" already exists, skipping`);
2445
+ continue;
2446
+ }
2447
+ // E2.a (W-mp7goxe4000p75f7): catch random-suffix and prefix-match
2448
+ // near-duplicates BEFORE writing. Logs + flags for human review;
2449
+ // no existing skills are deleted or renamed.
2450
+ const nearDup = _findNearDuplicateSkill(personalSkillRoot, name.replace(/[^a-z0-9-]/g, '-'));
2451
+ if (nearDup) {
2452
+ log('warn', `Skill "${name}" appears to be a near-duplicate of "${nearDup}", skipping`);
2453
+ const flaggedPath = path.join(ENGINE_DIR, 'skills-flagged.json');
2454
+ mutateJsonFileLocked(flaggedPath, (data) => {
2455
+ const arr = Array.isArray(data) ? data : [];
2456
+ arr.push({
2457
+ timestamp: ts(),
2458
+ candidate: name,
2459
+ existing: nearDup,
2460
+ agent: agentName,
2461
+ dispatchId: dispatchItem?.id || null,
2462
+ });
2463
+ return arr;
2464
+ });
2465
+ continue;
2426
2466
  }
2467
+ // Native skill format: only name + description in frontmatter. The
2468
+ // `Auto-extracted` marker is an HTML comment so the dashboard's
2469
+ // autoGenerated detection picks it up without polluting the body
2470
+ // an agent reads.
2471
+ const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
2472
+ const body = fmMatch[2] || '';
2473
+ const marker = `<!-- Auto-extracted by ${agentName} on ${dateStamp()} -->`;
2474
+ const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${marker}\n\n${body.trim()}\n`;
2475
+ if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
2476
+ shared.safeWrite(skillPath, ccContent);
2477
+ try { require('./queries').invalidateSkillsCache(); } catch {}
2478
+ log('info', `Extracted skill "${name}" from ${agentName} → ${skillPath}`);
2427
2479
 
2428
2480
  }
2429
2481
  }
@@ -488,16 +488,6 @@ function renderPlaybook(type, vars) {
488
488
  content += `- Gotchas or warnings for future agents\n`;
489
489
  content += `- Conventions to follow\n`;
490
490
  content += `- **SOURCE REFERENCES for every finding** — file paths with line numbers, PR URLs, API endpoints, config keys. Format: \`(source: path/to/file.ts:42)\` or \`(source: PR-12345)\`. Without references, findings cannot be verified.\n\n`;
491
- content += `### Skill Extraction (IMPORTANT)\n\n`;
492
- content += `If during this task you discovered a **repeatable workflow** — a multi-step procedure, workaround, build process, or pattern that other agents should follow in similar situations — only output it as a fenced skill block when **all** of these are true: (1) you had to discover it during this task, (2) it is not already captured in team memory, repo docs, existing playbooks, or existing skills, and (3) another agent is likely to reuse it on future tasks. **Zero skills is the default.** Prefer the inbox findings for one-off notes, repo facts, and task-specific observations.\n\n`;
493
- content += `Format your skill as a fenced code block with the \`skill\` language tag:\n\n`;
494
- content += '````\n```skill\n';
495
- content += `---\nname: short-descriptive-name\ndescription: One-line description of what this skill does\nallowed-tools: Bash, Read, Edit\ntrigger: when should an agent use this\nscope: minions\nproject: any\n---\n\n# Skill Title\n\n## Steps\n1. ...\n2. ...\n\n## Notes\n...\n`;
496
- content += '```\n````\n\n';
497
- content += `- Set \`scope: minions\` for cross-project or Minions-wide skills; the engine writes them to the selected runtime's native personal skills directory so they are available in normal runtime windows too\n`;
498
- content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to the selected runtime's native project skills directory\n`;
499
- content += `- Emit at most one skill block per task unless you uncovered two clearly distinct reusable workflows\n`;
500
- content += `- Do NOT create a skill for one-off bug fixes, isolated command output, obvious repo facts, or anything already covered by existing docs/playbooks/skills\n`;
501
491
 
502
492
  // Inject project-level variables from config
503
493
  const config = getConfig();
@@ -604,7 +594,7 @@ function buildSystemPrompt(agentId, config, project) {
604
594
  // Agent identity
605
595
  prompt += `# You are ${agent.name} (${agent.role})\n\n`;
606
596
  prompt += `Agent ID: ${agentId}\n`;
607
- prompt += `Skills: ${(agent.skills || []).join(', ')}\n\n`;
597
+ prompt += `Expertise: ${(agent.skills || []).join(', ')}\n\n`;
608
598
 
609
599
  // Charter (detailed instructions — typically 1-2KB)
610
600
  if (charter) {
@@ -624,12 +614,59 @@ function buildSystemPrompt(agentId, config, project) {
624
614
  prompt += `2. ${getRepoHostToolRule(project)}\n`;
625
615
  prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
626
616
  prompt += `4. Write learnings to the path specified in the task prompt (format: \`notes/inbox/{agent}-{work-item-id}-{date}-{time}.md\`)\n`;
627
- prompt += `5. Agent status is managed by the engine via dispatch.json — agents do not need to track their own status\n`;
628
- prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — minions-scoped skills are auto-extracted to the selected runtime's native personal skills directory\n\n`;
617
+ prompt += `5. Agent status is managed by the engine via dispatch.json — agents do not need to track their own status\n\n`;
629
618
 
630
619
  return prompt;
631
620
  }
632
621
 
622
+ // E2.b (W-mp7goxe4000p75f7): build the `## Existing Skills (do not duplicate)`
623
+ // section for buildAgentContext. Lists name + first-line description for every
624
+ // personal-scope/installed-plugin skill discovered by queries.getSkills().
625
+ // Caps at ~2KB by truncating alphabetically with a "_...and N more_" tail.
626
+ // Returns '' when there are no personal-scope skills to advertise.
627
+ const EXISTING_SKILLS_MAX_BYTES = 2048;
628
+ const PERSONAL_SKILL_SCOPES = new Set(['claude-code', 'copilot', 'agent-skill', 'plugin', 'copilot-plugin']);
629
+
630
+ function _buildExistingSkillsSection(config) {
631
+ let skills;
632
+ try {
633
+ skills = queries.getSkills(config) || [];
634
+ } catch { return ''; }
635
+ const personal = skills
636
+ .filter(s => PERSONAL_SKILL_SCOPES.has(s.scope))
637
+ .map(s => ({
638
+ name: String(s.name || '').trim(),
639
+ description: String(s.description || '').split('\n')[0].trim(),
640
+ }))
641
+ .filter(s => s.name)
642
+ .sort((a, b) => a.name.localeCompare(b.name));
643
+ if (personal.length === 0) return '';
644
+
645
+ const header = `## Existing Skills (do not duplicate)\n\nThese personal-scope skills already exist on disk. Do NOT emit a \`\`\`skill block whose name duplicates or near-duplicates one of these — the engine will flag it for human review and skip the write.\n\n`;
646
+ const lines = personal.map(s => s.description ? `- ${s.name} — ${s.description}` : `- ${s.name}`);
647
+
648
+ let body = lines.join('\n') + '\n\n';
649
+ if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
650
+ return header + body;
651
+ }
652
+
653
+ // Truncate alphabetically until we fit, leaving room for the tail line.
654
+ let kept = lines.length;
655
+ while (kept > 0) {
656
+ const dropped = lines.length - kept;
657
+ const tail = `_...and ${dropped} more (alphabetical truncation; full list on disk)_\n\n`;
658
+ body = lines.slice(0, kept).join('\n') + '\n' + tail;
659
+ if (Buffer.byteLength(header + body, 'utf8') <= EXISTING_SKILLS_MAX_BYTES) {
660
+ return header + body;
661
+ }
662
+ kept--;
663
+ }
664
+ // Pathological: header alone exceeds the cap. Return at least the header
665
+ // with a tail showing every skill was dropped, so the agent still sees the
666
+ // section heading.
667
+ return header + `_...and ${lines.length} more (alphabetical truncation; full list on disk)_\n\n`;
668
+ }
669
+
633
670
  // Bulk context: history, notes, conventions, skills — prepended to user/task prompt.
634
671
  // This is the content that grows over time and would bloat the system prompt.
635
672
  function buildAgentContext(agentId, config, project) {
@@ -682,7 +719,12 @@ function buildAgentContext(agentId, config, project) {
682
719
 
683
720
  appendIndex('Knowledge Base Reference', getKnowledgeBaseIndex(), 8192);
684
721
 
685
- context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`. Runtime-native skills and commands are left to the selected CLI runtime; Minions does not inject their contents into the task prompt.\n\n`;
722
+ context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`. Runtime-native skills and commands live in their native indexes (the catalog of names + descriptions is injected below as "Existing Skills"; full skill bodies are not).\n\n`;
723
+
724
+ // E2.b (W-mp7goxe4000p75f7): inject existing personal-scope skill catalog
725
+ // so agents can see what already exists before emitting a near-duplicate
726
+ // skill block. Caps at ~2KB; truncates alphabetically with a tail count.
727
+ context += _buildExistingSkillsSection(config);
686
728
 
687
729
  // Minions awareness: what's in flight, who's doing what
688
730
  const dispatch = getDispatch();
@@ -148,85 +148,6 @@ function tryBorrow(projectName, dispatchId, opts) {
148
148
  return borrowed;
149
149
  }
150
150
 
151
- /**
152
- * Register a brand-new worktree as a borrowed pool member. Used when a fresh
153
- * worktree is created (no idle slot available) and the pool has room — we
154
- * track it from creation so the eventual return is a simple state flip.
155
- *
156
- * Honors capacity inside the locked mutation: if the project is already at or
157
- * over `poolSize`, the registration is rejected and the worktree stays
158
- * untracked (cleanup will reap it normally).
159
- */
160
- function registerBorrowed(projectName, wtPath, dispatchId, opts) {
161
- opts = opts || {};
162
- if (!projectName || !wtPath || !dispatchId) return false;
163
- const poolSize = Number.isFinite(opts.poolSize) ? Math.max(0, Math.floor(opts.poolSize)) : 0;
164
- if (poolSize <= 0) return false;
165
- const branch = opts.branch || '';
166
- const now = Number.isFinite(opts.now) ? opts.now : Date.now();
167
- let registered = false;
168
- mutateWorktreePool((data) => {
169
- const entries = data.entries;
170
- // Already tracked? Update the borrower fields and keep going.
171
- const existingIdx = _findEntryByPath(entries, wtPath);
172
- if (existingIdx !== -1) {
173
- const e = entries[existingIdx];
174
- e.state = POOL_STATE.BORROWED;
175
- e.borrowedBy = dispatchId;
176
- e.borrowedAt = _nowIso(now);
177
- e.idleSince = null;
178
- if (branch) e.lastBranch = branch;
179
- e.lastUsed = e.borrowedAt;
180
- registered = true;
181
- return data;
182
- }
183
- // Capacity gate — count entries (idle + borrowed) for this project.
184
- const projectCount = entries.filter(e => e && e.project === projectName).length;
185
- if (projectCount >= poolSize) return data;
186
- entries.push({
187
- project: projectName,
188
- path: wtPath,
189
- state: POOL_STATE.BORROWED,
190
- borrowedBy: dispatchId,
191
- borrowedAt: _nowIso(now),
192
- idleSince: null,
193
- lastBranch: branch,
194
- createdAt: _nowIso(now),
195
- lastUsed: _nowIso(now),
196
- });
197
- registered = true;
198
- return data;
199
- });
200
- return registered;
201
- }
202
-
203
- /**
204
- * Mark a borrowed entry as idle. Caller is responsible for having already run
205
- * the git reset/clean/checkout-detach/pull dance — this is purely a state
206
- * flip. If the entry is unknown, no-op (caller can decide to register first).
207
- */
208
- function markIdle(wtPath, opts) {
209
- opts = opts || {};
210
- if (!wtPath) return false;
211
- const now = Number.isFinite(opts.now) ? opts.now : Date.now();
212
- const branch = opts.branch || '';
213
- let flipped = false;
214
- mutateWorktreePool((data) => {
215
- const idx = _findEntryByPath(data.entries, wtPath);
216
- if (idx === -1) return data;
217
- const e = data.entries[idx];
218
- e.state = POOL_STATE.IDLE;
219
- e.borrowedBy = null;
220
- e.borrowedAt = null;
221
- e.idleSince = _nowIso(now);
222
- e.lastUsed = _nowIso(now);
223
- if (branch) e.lastBranch = branch;
224
- flipped = true;
225
- return data;
226
- });
227
- return flipped;
228
- }
229
-
230
151
  /**
231
152
  * Return a worktree to the pool — flip an existing borrowed entry to idle, or
232
153
  * insert a fresh entry as idle when the project is under capacity. Both paths
@@ -353,8 +274,8 @@ function pruneStale(opts) {
353
274
  }
354
275
 
355
276
  /**
356
- * Count entries (idle + borrowed) belonging to a project. Used to gate
357
- * `markIdle` registrations from fresh-create paths.
277
+ * Count entries (idle + borrowed) belonging to a project. Used for capacity
278
+ * gating diagnostics and pool-size dashboards.
358
279
  */
359
280
  function countForProject(projectName) {
360
281
  if (!projectName) return 0;
@@ -397,8 +318,6 @@ module.exports = {
397
318
  isPoolMember,
398
319
  getEntry,
399
320
  tryBorrow,
400
- registerBorrowed,
401
- markIdle,
402
321
  returnToPool,
403
322
  evictEntry,
404
323
  pruneStale,
package/engine.js CHANGED
@@ -990,8 +990,15 @@ async function spawnAgent(dispatchItem, config) {
990
990
  if (borrowed && borrowed.path && fs.existsSync(borrowed.path)) {
991
991
  try { shared.assertWorktreeOutsideProject(borrowed.path, rootDir); }
992
992
  catch (assertErr) {
993
+ // Always evict before deciding what to do — leaves no BORROWED
994
+ // orphan tied to a dispatch that won't complete normally.
995
+ // pruneStale would self-heal in ~10 ticks via orphan-borrow, but
996
+ // that window is unnecessary.
997
+ const _evictReason = assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT'
998
+ ? 'nested-in-project'
999
+ : 'assert-failed';
1000
+ worktreePool.evictEntry(borrowed.path, _evictReason);
993
1001
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') {
994
- worktreePool.evictEntry(borrowed.path, 'nested-in-project');
995
1002
  _failWorktreePreflight(assertErr); return null;
996
1003
  }
997
1004
  throw assertErr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1959",
3
+ "version": "0.1.1961",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"
@@ -60,7 +60,6 @@ Use subagents only for genuinely parallel, independent tasks. For reading files,
60
60
  - Use the appropriate repo-host tooling for PR creation. For Azure DevOps, prefer the `az` CLI first and use ADO MCP only as a fallback when `az` is unavailable or insufficient.
61
61
  - Do NOT checkout branches in the main working tree — use worktrees.
62
62
  - Read `notes.md` for all team rules before starting.
63
- - Only emit a ```skill block if you uncovered a durable reusable workflow that is not already documented and is likely to help future tasks; zero skills is the default, and one-off findings belong in the inbox notes instead.
64
63
 
65
64
  ## When to Stop
66
65
 
@@ -46,20 +46,7 @@ Bias toward senior-engineer restraint:
46
46
  5. Previous agent output in `agents/*/live-output.log` for related tasks
47
47
  6. Work item descriptions and `resultSummary` for prior completed work on the same topic
48
48
  Only after exhausting team memory should you look outside (web search, codebase exploration, external docs). This avoids duplicating research another agent already completed and ensures team decisions are respected.
49
- - Only output a fenced skill block when **all** of these are true: (1) you discovered a durable multi-step workflow that was not already documented in team memory, repo docs, existing playbooks, or existing skills, (2) another agent is likely to need it on future tasks, and (3) the workflow is specific enough to be actionable but general enough to stand alone. **Zero skills is the default.** Prefer writing one-off findings, repo facts, or task-specific notes to the inbox findings instead of creating a skill. Emit **at most one skill block per task** unless the task clearly uncovered two unrelated reusable workflows. The engine auto-extracts valid skill blocks to `~/.claude/skills/<name>/SKILL.md`, so `scope: minions` skills become user-level Claude skills available in normal Claude windows too. Required format:
50
- ````
51
- ```skill
52
- ---
53
- name: skill-name-here
54
- description: One-line description of when to trigger this skill
55
- scope: minions
56
- ---
57
-
58
- Instructions for the skill go here.
59
- ```
60
- ````
61
- The `name` and `description` fields are required. `scope` defaults to `minions` (global). Use `scope: minions` for user-level reusable skills; use `scope: project` + `project: ProjectName` only for repo-specific skills that should land in that project via PR.
62
- Do **not** create a skill for one-off bug fixes, isolated command outputs, obvious repo facts, or anything already covered by existing docs/playbooks/skills.
49
+ - Only output a fenced skill block when **all** of these are true: (1) you discovered a durable multi-step workflow that was not already documented in team memory, repo docs, existing playbooks, or existing skills, (2) another agent is likely to need it on future tasks, and (3) the workflow is specific enough to be actionable but general enough to stand alone. **Zero skills is the default.** Prefer writing one-off findings, repo facts, or task-specific notes to the inbox findings instead of creating a skill. Emit at most one skill block per task unless the task clearly uncovered two unrelated reusable workflows. The engine auto-extracts valid skill blocks to the selected runtime's native personal skills directory, so `scope: minions` skills become user-level Claude/Copilot skills available in normal runtime windows too. See [`docs/skills.md`](../docs/skills.md) for the skill block format. Do not create a skill for one-off bug fixes, isolated command output, obvious repo facts, or anything already covered by existing docs/playbooks/skills.
63
50
  - Do TDD where it makes sense — write failing tests first, then implement, then verify tests pass. Especially for bug fixes (write a test that reproduces the bug) and new utility functions.
64
51
 
65
52
  ## Completion Reports