u-foo 2.3.18 → 2.3.20

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/README.md CHANGED
@@ -143,7 +143,9 @@ ufoo resume <ucode|uclaude|ucodex|nickname>
143
143
  ufoo recover list
144
144
  ```
145
145
 
146
- Common chat commands include `/status`, `/bus list`, `/bus status`, `/settings`, `/project list`, `/project switch <index|path>`, `/open <path>`, `/resume list`, `/group status`, and `@nickname <message>`.
146
+ Common chat commands include `/status`, `/bus list`, `/bus status`, `/settings`, `/project list`, `/project switch <index|path>`, `/open <path>`, `/resume list`, `/group status`, `/skills`, and `@nickname <message>`.
147
+
148
+ In `ufoo chat`, `/skills` lists ufoo's built-in available skills and preset workflow capabilities so users can discover and choose them. It does not execute a task by itself, and it is not a private capability list for any one agent.
147
149
 
148
150
  ### Event Bus
149
151
 
@@ -197,7 +199,7 @@ Use decisions sparingly for plan-level constraints. Durable project facts belong
197
199
  | Online | `ufoo online server|token|room|channel|connect|send|inbox` |
198
200
  | History | `ufoo history build|show|prompt` |
199
201
  | Skills | `ufoo skills list|install` |
200
- | Chat settings | `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
202
+ | Chat commands | `/skills`, `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
201
203
 
202
204
  ### Groups
203
205
 
@@ -246,8 +248,12 @@ Use the low-level queue runtime:
246
248
  ucode-core submit --tool read --args-json '{"path":"README.md"}' --json
247
249
  ucode-core run-once --json
248
250
  ucode-core list --json
251
+ ucode-core skills list --json
252
+ ucode-core skills show <name>
249
253
  ```
250
254
 
255
+ `ucode-core skills list` discovers ufoo/ucode built-in and local `SKILL.md` preset workflow capabilities for selection. It lists metadata only; full skill bodies are loaded by ucode only when the user explicitly references a skill such as `$demo` or a direct `SKILL.md` link.
256
+
251
257
  ## Configuration
252
258
 
253
259
  Project configuration is stored in `.ufoo/config.json`. `ucode` provider credentials are stored globally in `~/.ufoo/config.json` and merged into project config at load time.
package/README.zh-CN.md CHANGED
@@ -133,7 +133,9 @@ ufoo resume <ucode|uclaude|ucodex|nickname>
133
133
  ufoo recover list
134
134
  ```
135
135
 
136
- 常见 chat 命令包括 `/status`、`/bus list`、`/bus status`、`/settings`、`/project list`、`/project switch <index|path>`、`/open <path>`、`/resume list`、`/group status` 和 `@nickname <message>`。
136
+ 常见 chat 命令包括 `/status`、`/bus list`、`/bus status`、`/settings`、`/project list`、`/project switch <index|path>`、`/open <path>`、`/resume list`、`/group status`、`/skills` 和 `@nickname <message>`。
137
+
138
+ 在 `ufoo chat` 中,`/skills` 用于列出 ufoo 内置的可用 skills 和预设工作流能力,方便用户发现和选择。它本身不等于执行任务,也不表示某个 agent 的私有能力列表。
137
139
 
138
140
  ### 事件总线
139
141
 
@@ -211,8 +213,12 @@ ufoo ucode build
211
213
  ucode-core submit --tool read --args-json '{"path":"README.md"}' --json
212
214
  ucode-core run-once --json
213
215
  ucode-core list --json
216
+ ucode-core skills list --json
217
+ ucode-core skills show <name>
214
218
  ```
215
219
 
220
+ `ucode-core skills list` 用于发现 ufoo/ucode 内置和本地 `SKILL.md` 预设工作流能力,输出的是供选择的元数据;完整 skill 内容只会在用户显式引用 `$demo` 或直接链接某个 `SKILL.md` 时由 ucode 加载。
221
+
216
222
  ## 命令参考
217
223
 
218
224
  | 范围 | 命令 |
@@ -228,7 +234,7 @@ ucode-core list --json
228
234
  | Online | `ufoo online server|token|room|channel|connect|send|inbox` |
229
235
  | History | `ufoo history build|show|prompt` |
230
236
  | Skills | `ufoo skills list|install` |
231
- | Chat 设置 | `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
237
+ | Chat 命令 | `/skills`, `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
232
238
 
233
239
  ## 配置
234
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.18",
3
+ "version": "2.3.20",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -1,6 +1,9 @@
1
1
  const os = require("os");
2
2
  const { version: packageVersion } = require("../../package.json");
3
3
 
4
+ const ANSI_RESET = "\x1b[0m";
5
+ const CLAUDE_ORANGE = "\x1b[38;2;217;119;87m";
6
+
4
7
  function createAgentViewController(options = {}) {
5
8
  const {
6
9
  screen,
@@ -39,6 +42,7 @@ function createAgentViewController(options = {}) {
39
42
  sendBusMessage = () => {},
40
43
  sendResize = () => {},
41
44
  requestScreenSnapshot = () => {},
45
+ sendBusWatch = () => {},
42
46
  } = options;
43
47
 
44
48
  if (!screen || typeof screen.render !== "function") {
@@ -55,6 +59,8 @@ function createAgentViewController(options = {}) {
55
59
  let busInputValue = "";
56
60
  let busInputCursor = 0;
57
61
  let busLogLines = [];
62
+ let busStartupAgentId = "";
63
+ let busStartupLineCount = 0;
58
64
  const originalRender = screen.render.bind(screen);
59
65
  let renderFrozen = false;
60
66
 
@@ -64,10 +70,14 @@ function createAgentViewController(options = {}) {
64
70
  };
65
71
 
66
72
  function getRows() {
73
+ if (Number.isFinite(screen.height) && screen.height > 0) return screen.height;
74
+ if (Number.isFinite(screen.rows) && screen.rows > 0) return screen.rows;
67
75
  return processStdout.rows || 24;
68
76
  }
69
77
 
70
78
  function getCols() {
79
+ if (Number.isFinite(screen.width) && screen.width > 0) return screen.width;
80
+ if (Number.isFinite(screen.cols) && screen.cols > 0) return screen.cols;
71
81
  return processStdout.columns || 80;
72
82
  }
73
83
 
@@ -76,6 +86,10 @@ function createAgentViewController(options = {}) {
76
86
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
77
87
  }
78
88
 
89
+ function hasAnsi(text = "") {
90
+ return /\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[0-9;?]*[ -/]*[@-~])/.test(String(text || ""));
91
+ }
92
+
79
93
  function clamp(value, min, max) {
80
94
  const normalized = Number.isFinite(value) ? Math.floor(value) : min;
81
95
  return Math.max(min, Math.min(max, normalized));
@@ -141,6 +155,39 @@ function createAgentViewController(options = {}) {
141
155
  return `${truncateToWidth(clean, normalizedWidth - 1).trimEnd()}…`;
142
156
  }
143
157
 
158
+ function fitAnsiText(text = "", width = 1) {
159
+ const normalizedWidth = Math.max(1, width);
160
+ const raw = String(text || "").replace(/\r/g, "");
161
+ if (!hasAnsi(raw)) return fitText(raw, normalizedWidth);
162
+ if (displayWidth(raw) <= normalizedWidth) {
163
+ return padToWidth(raw, normalizedWidth);
164
+ }
165
+ if (normalizedWidth <= 1) return "…";
166
+
167
+ const ansiPattern = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[ -/]*[@-~]/g;
168
+ let out = "";
169
+ let cells = 0;
170
+ let index = 0;
171
+ while (index < raw.length && cells < normalizedWidth - 1) {
172
+ ansiPattern.lastIndex = index;
173
+ const match = ansiPattern.exec(raw);
174
+ if (match && match.index === index) {
175
+ out += match[0];
176
+ index += match[0].length;
177
+ continue;
178
+ }
179
+ const char = Array.from(raw.slice(index))[0] || "";
180
+ if (!char) break;
181
+ const charWidth = charDisplayWidth(char);
182
+ if (cells + charWidth > normalizedWidth - 1) break;
183
+ out += char;
184
+ cells += charWidth;
185
+ index += char.length;
186
+ }
187
+ const suffix = `${ANSI_RESET}…`;
188
+ return padToWidth(`${out}${suffix}`, normalizedWidth);
189
+ }
190
+
144
191
  function horizontalLine(width = 80) {
145
192
  return "─".repeat(Math.max(1, width));
146
193
  }
@@ -149,6 +196,11 @@ function createAgentViewController(options = {}) {
149
196
  return fitText(text, Math.max(1, width));
150
197
  }
151
198
 
199
+ function logLine(text = "", width = 80) {
200
+ const normalizedWidth = Math.max(1, width);
201
+ return hasAnsi(text) ? fitAnsiText(text, normalizedWidth) : plainLine(text, normalizedWidth);
202
+ }
203
+
152
204
  function sliceDisplayCells(text = "", startCell = 0, maxCells = 1) {
153
205
  const targetStart = Math.max(0, startCell);
154
206
  const targetWidth = Math.max(1, maxCells);
@@ -174,6 +226,7 @@ function createAgentViewController(options = {}) {
174
226
 
175
227
  function wrapTextLine(text = "", width = 80) {
176
228
  const inner = Math.max(1, width);
229
+ if (hasAnsi(text)) return [String(text || "")];
177
230
  const clean = stripAnsi(String(text || ""));
178
231
  if (!clean) return [""];
179
232
  const lines = [];
@@ -209,11 +262,14 @@ function createAgentViewController(options = {}) {
209
262
  function forceScreenRepaint() {
210
263
  if (typeof screen.realloc === "function") {
211
264
  screen.realloc();
212
- return;
213
- }
214
- if (typeof screen.alloc === "function") {
265
+ } else if (typeof screen.alloc === "function") {
215
266
  screen.alloc(true);
216
267
  }
268
+ try {
269
+ originalRender();
270
+ } catch {
271
+ // Ignore repaint failures while restoring from raw agent view.
272
+ }
217
273
  }
218
274
 
219
275
  function compactProjectPath(projectRoot = "") {
@@ -247,16 +303,21 @@ function createAgentViewController(options = {}) {
247
303
  function buildClaudeStartupLines(agentLabel = "", width = 80) {
248
304
  const label = String(agentLabel || "").trim();
249
305
  const projectPath = compactProjectPath(getProjectRoot());
250
- const product = "Claude Code";
306
+ const product = "ClaudeCode";
251
307
  const detail = label ? `${label} · managed headless` : "managed headless";
308
+ const iconWidth = 9;
309
+ const iconLine = (icon = "", text = "") => {
310
+ const pad = " ".repeat(Math.max(0, iconWidth - displayWidth(icon)));
311
+ return `${CLAUDE_ORANGE}${icon}${ANSI_RESET}${pad}${text}`;
312
+ };
252
313
  const lines = [
253
- ` ▐▛███▜▌${product} v${packageVersion}`,
254
- `▝▜█████▛▘${detail}`,
255
- ` ▘▘▝▝${projectPath}`,
314
+ iconLine("▐▛███▜▌", `${product}v${packageVersion}`),
315
+ iconLine("▝▜█████▛▘", detail),
316
+ iconLine(" ▘▘▝▝", projectPath),
256
317
  "",
257
318
  ];
258
319
  if (width < 44) return lines;
259
- return lines.map((line) => padToWidth(line, Math.min(58, Math.max(1, width))));
320
+ return lines.map((line) => fitAnsiText(line, Math.min(58, Math.max(1, width))));
260
321
  }
261
322
 
262
323
  function buildCodexStartupLines(agentLabel = "", width = 80) {
@@ -288,11 +349,31 @@ function createAgentViewController(options = {}) {
288
349
  return buildClaudeStartupLines(agentLabel || agentId, width);
289
350
  }
290
351
 
352
+ function staticStartupLines(agentId = "", agentLabel = "", width = 80) {
353
+ const lines = buildInternalStartupLines(agentId, agentLabel, width);
354
+ if (lines.length > 0 && String(lines[lines.length - 1] || "") === "") {
355
+ return lines.slice(0, -1);
356
+ }
357
+ return lines;
358
+ }
359
+
291
360
  function resetBusView(agentId) {
292
361
  busInputValue = "";
293
362
  busInputCursor = 0;
363
+ busStartupAgentId = agentId || "";
294
364
  const label = getAgentLabel(agentId);
295
- busLogLines = buildInternalStartupLines(agentId, label, getCols());
365
+ const startupLines = staticStartupLines(agentId, label, getCols());
366
+ busLogLines = startupLines.concat("");
367
+ busStartupLineCount = startupLines.length;
368
+ }
369
+
370
+ function refreshBusStartupLines(width = getCols()) {
371
+ if (!busStartupAgentId || busStartupLineCount <= 0) return;
372
+ const label = getAgentLabel(busStartupAgentId);
373
+ const startupLines = staticStartupLines(busStartupAgentId, label, width);
374
+ const tailLines = busLogLines.slice(busStartupLineCount);
375
+ busLogLines = startupLines.concat(tailLines.length > 0 ? tailLines : [""]);
376
+ busStartupLineCount = startupLines.length;
296
377
  }
297
378
 
298
379
  function appendBusLog(text = "") {
@@ -331,6 +412,7 @@ function createAgentViewController(options = {}) {
331
412
  const rows = getRows();
332
413
  const cols = getCols();
333
414
  const width = Math.max(20, cols);
415
+ refreshBusStartupLines(width);
334
416
  const inputTop = Math.max(4, rows - 3);
335
417
  const logContentTop = 1;
336
418
  const logContentBottom = Math.max(logContentTop, inputTop - 1);
@@ -339,7 +421,7 @@ function createAgentViewController(options = {}) {
339
421
  processStdout.write("\x1b[?25l");
340
422
  const visibleLines = getWrappedBusLogLines(width).slice(-logContentHeight);
341
423
  for (let i = 0; i < logContentHeight; i += 1) {
342
- writeAt(logContentTop + i, plainLine(visibleLines[i] || "", width));
424
+ writeAt(logContentTop + i, logLine(visibleLines[i] || "", width));
343
425
  }
344
426
 
345
427
  writeAt(inputTop, horizontalLine(width));
@@ -392,6 +474,7 @@ function createAgentViewController(options = {}) {
392
474
  function enterAgentView(agentId, options = {}) {
393
475
  if (currentView === "agent" && viewingAgent === agentId) return;
394
476
  if (currentView === "agent") {
477
+ if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
395
478
  disconnectAgentOutput();
396
479
  disconnectAgentInput();
397
480
  }
@@ -416,6 +499,7 @@ function createAgentViewController(options = {}) {
416
499
  agentInputSuppressUntil = now() + 300;
417
500
  agentViewUsesBus = Boolean(options.useBus);
418
501
  if (agentViewUsesBus) {
502
+ sendBusWatch(agentId, true);
419
503
  resetBusView(agentId);
420
504
  renderBusView();
421
505
  } else {
@@ -439,12 +523,15 @@ function createAgentViewController(options = {}) {
439
523
 
440
524
  disconnectAgentOutput();
441
525
  disconnectAgentInput();
526
+ if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
442
527
  agentViewUsesBus = false;
443
528
  agentOutputSuppressed = false;
444
529
  agentBarVisible = false;
445
530
  busInputValue = "";
446
531
  busInputCursor = 0;
447
532
  busLogLines = [];
533
+ busStartupAgentId = "";
534
+ busStartupLineCount = 0;
448
535
 
449
536
  currentView = "main";
450
537
  viewingAgent = null;
@@ -462,11 +549,11 @@ function createAgentViewController(options = {}) {
462
549
  setDashboardView("agents");
463
550
  setSelectedAgentIndex(-1);
464
551
  setScreenGrabKeys(false);
465
- forceScreenRepaint();
466
552
  clearTargetAgent();
467
553
  renderDashboard();
468
554
  focusInput();
469
555
  resizeInput();
556
+ forceScreenRepaint();
470
557
  try {
471
558
  if (screen.program && typeof screen.program.showCursor === "function") {
472
559
  screen.program.showCursor();
@@ -448,12 +448,12 @@ function createCommandExecutor(options = {}) {
448
448
  try {
449
449
  const skills = createSkills(projectRoot);
450
450
 
451
- if (subcommand === "list") {
451
+ if (!subcommand || subcommand === "list") {
452
452
  const skillList = skills.list();
453
453
  if (skillList.length === 0) {
454
- logMessage("system", "{white-fg}No skills found{/white-fg}");
454
+ logMessage("system", "{white-fg}No built-in ufoo skills found{/white-fg}");
455
455
  } else {
456
- logMessage("system", `{cyan-fg}Available skills:{/cyan-fg} ${skillList.length}`);
456
+ logMessage("system", `{cyan-fg}Available built-in ufoo skills:{/cyan-fg} ${skillList.length}`);
457
457
  for (const skill of skillList) {
458
458
  logMessage("system", ` • ${skill}`);
459
459
  }
@@ -129,10 +129,10 @@ const COMMAND_TREE = {
129
129
  },
130
130
  },
131
131
  "/skills": {
132
- desc: "Skills management",
132
+ desc: "List ufoo built-in skills and preset workflows",
133
133
  children: {
134
- install: { desc: "Install skills (use: all or name)" },
135
- list: { desc: "List available skills" },
134
+ install: { desc: "Install built-in skills (use: all or name)" },
135
+ list: { desc: "List built-in skills for discovery" },
136
136
  },
137
137
  },
138
138
  "/status": { desc: "Status display" },
@@ -19,6 +19,8 @@ function createDaemonMessageRouter(options = {}) {
19
19
  getCurrentView = () => "main",
20
20
  isAgentViewUsesBus = () => false,
21
21
  getViewingAgent = () => "",
22
+ isAgentEventForViewingAgent = (data, viewingAgent, publisher) =>
23
+ Boolean(viewingAgent && (publisher === viewingAgent || data?.target === viewingAgent)),
22
24
  writeToAgentTerm = () => {},
23
25
  consumePendingDelivery = () => false,
24
26
  getPendingState = () => null,
@@ -352,6 +354,19 @@ function createDaemonMessageRouter(options = {}) {
352
354
  function handleBusMessage(msg) {
353
355
  const data = msg.data || {};
354
356
  if (data.event === "activity_state_changed") {
357
+ const publisher = data.publisher && data.publisher !== "unknown"
358
+ ? data.publisher
359
+ : (data.subscriber || "bus");
360
+ const viewingAgent = getViewingAgent();
361
+ if (
362
+ getCurrentView() === "agent" &&
363
+ isAgentViewUsesBus() &&
364
+ viewingAgent &&
365
+ isAgentEventForViewingAgent(data, viewingAgent, publisher)
366
+ ) {
367
+ const state = data.state || (data.data && data.data.state) || "";
368
+ if (state) writeToAgentTerm(`[state] ${state}\r\n`);
369
+ }
355
370
  requestStatus();
356
371
  return true;
357
372
  }
@@ -375,11 +390,12 @@ function createDaemonMessageRouter(options = {}) {
375
390
  if (data.silent && !streamPayload) return true;
376
391
  if (!displayMessage && !streamPayload) return true;
377
392
 
393
+ const viewingAgent = getViewingAgent();
378
394
  const isAgentViewTarget =
379
395
  getCurrentView() === "agent" &&
380
396
  isAgentViewUsesBus() &&
381
- getViewingAgent() &&
382
- publisher === getViewingAgent();
397
+ viewingAgent &&
398
+ isAgentEventForViewingAgent(data, viewingAgent, publisher);
383
399
 
384
400
  const displayName = resolveAgentDisplayName(publisher);
385
401
 
package/src/chat/index.js CHANGED
@@ -1581,6 +1581,14 @@ async function runChat(projectRoot, options = {}) {
1581
1581
  source: "chat-internal-agent-view",
1582
1582
  });
1583
1583
  },
1584
+ sendBusWatch: (agentId, enabled) => {
1585
+ if (!agentId) return;
1586
+ send({
1587
+ type: IPC_REQUEST_TYPES.BUS_WATCH,
1588
+ agent_id: agentId,
1589
+ enabled: enabled !== false,
1590
+ });
1591
+ },
1584
1592
  sendResize: (cols, rows) => {
1585
1593
  sendResizeWithCapabilities(cols, rows);
1586
1594
  },
@@ -1612,6 +1620,21 @@ async function runChat(projectRoot, options = {}) {
1612
1620
  getCurrentView: () => getCurrentView(),
1613
1621
  isAgentViewUsesBus: () => isAgentViewUsesBus(),
1614
1622
  getViewingAgent: () => getViewingAgent(),
1623
+ isAgentEventForViewingAgent: (data, viewingAgent, publisher) => {
1624
+ if (!viewingAgent) return false;
1625
+ const label = getAgentLabel(viewingAgent);
1626
+ const candidates = [
1627
+ publisher,
1628
+ data && data.publisher,
1629
+ data && data.target,
1630
+ data && data.subscriber,
1631
+ ].filter(Boolean);
1632
+ return candidates.some((value) => (
1633
+ value === viewingAgent ||
1634
+ value === label ||
1635
+ resolveAgentId(value) === viewingAgent
1636
+ ));
1637
+ },
1615
1638
  writeToAgentTerm,
1616
1639
  consumePendingDelivery,
1617
1640
  getPendingState,
package/src/code/agent.js CHANGED
@@ -25,6 +25,12 @@ const {
25
25
  loadSessionSnapshot,
26
26
  } = require("./sessionStore");
27
27
  const { buildPromptContext } = require("./prompts");
28
+ const {
29
+ buildSkillInjections,
30
+ formatSkillsList,
31
+ listUcodeSkills,
32
+ showSkill,
33
+ } = require("./skills");
28
34
 
29
35
  function printPrompt() {
30
36
  process.stdout.write("> ");
@@ -243,6 +249,61 @@ function createToolLogCollector(logs = [], onToolLog = null) {
243
249
  };
244
250
  }
245
251
 
252
+ function pushSkillWarning(logs = [], onToolLog = null, warning = "") {
253
+ const text = String(warning || "").trim();
254
+ if (!text) return null;
255
+ const entry = {
256
+ type: "skills",
257
+ phase: "warning",
258
+ message: text,
259
+ error: text,
260
+ };
261
+ if (Array.isArray(logs)) logs.push(entry);
262
+ if (typeof onToolLog === "function") {
263
+ try {
264
+ onToolLog(entry);
265
+ } catch {
266
+ // ignore callback failures
267
+ }
268
+ }
269
+ return entry;
270
+ }
271
+
272
+ function stripSkillBlocksFromText(value = "") {
273
+ return String(value || "")
274
+ .replace(/<skill>\s*[\s\S]*?<\/skill>\s*/g, "")
275
+ .trim();
276
+ }
277
+
278
+ function stripSkillBlocksFromMessages(messages = []) {
279
+ if (!Array.isArray(messages)) return [];
280
+ return messages.map((message) => {
281
+ if (!message || typeof message !== "object" || Array.isArray(message)) return message;
282
+ if (typeof message.content === "string") {
283
+ return {
284
+ ...message,
285
+ content: stripSkillBlocksFromText(message.content),
286
+ };
287
+ }
288
+ if (Array.isArray(message.content)) {
289
+ return {
290
+ ...message,
291
+ content: message.content.map((item) => {
292
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
293
+ if (typeof item.text === "string") {
294
+ return {
295
+ ...item,
296
+ text: stripSkillBlocksFromText(item.text),
297
+ };
298
+ }
299
+ return item;
300
+ }),
301
+ };
302
+ }
303
+ return message;
304
+ });
305
+ }
306
+
246
307
  function isProjectAnalysisTask(task = "") {
247
308
  const text = String(task || "").trim().toLowerCase();
248
309
  if (!text) return false;
@@ -398,6 +459,16 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
398
459
  const taskPrompt = analysisTask
399
460
  ? `${taskText}\n\nAnalysis requirements:\n- Inspect repository evidence before concluding.\n- Cite concrete file observations.\n- Keep findings concise and actionable.`
400
461
  : taskText;
462
+ const skillInjections = buildSkillInjections({
463
+ prompt: taskPrompt,
464
+ workspaceRoot,
465
+ });
466
+ for (const warning of skillInjections.warnings || []) {
467
+ pushSkillWarning(logs, onToolLog, warning);
468
+ }
469
+ const effectiveTaskPrompt = Array.isArray(skillInjections.blocks) && skillInjections.blocks.length > 0
470
+ ? `${skillInjections.blocks.join("\n\n")}\n\n${taskPrompt}`
471
+ : taskPrompt;
401
472
  const systemContext = [String(state.context || "").trim(), preflightContext]
402
473
  .filter(Boolean)
403
474
  .join("\n\n");
@@ -422,7 +493,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
422
493
  workspaceRoot,
423
494
  provider,
424
495
  model,
425
- prompt: taskPrompt,
496
+ prompt: effectiveTaskPrompt,
426
497
  systemPrompt: systemContext,
427
498
  messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
428
499
  sessionId: String(sessionIdValue || ""),
@@ -440,7 +511,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
440
511
  // Use decomposed runner for bug fix tasks
441
512
  if (useDecomposition) {
442
513
  const decomposedResult = await runDecomposedTask({
443
- task: taskText,
514
+ task: effectiveTaskPrompt,
444
515
  state,
445
516
  onProgress: options.onProgress,
446
517
  onToolEvent: pushToolLog,
@@ -497,7 +568,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
497
568
  state.sessionId = cliRes.sessionId.trim();
498
569
  }
499
570
  if (cliRes && Array.isArray(cliRes.messages)) {
500
- state.nlMessages = cliRes.messages;
571
+ state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
501
572
  }
502
573
  const normalized = String(cliRes.output || "").trim();
503
574
  const summary = extractJsonSummary(normalized);
@@ -1235,6 +1306,8 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1235
1306
  " help",
1236
1307
  " exit|quit",
1237
1308
  " ubus|/ubus",
1309
+ " skills [list]",
1310
+ " skills show <name>",
1238
1311
  " bg|/bg <task>",
1239
1312
  " resume <session-id>",
1240
1313
  " tool <read|write|edit|bash> <args-json>",
@@ -1254,6 +1327,45 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1254
1327
  kind: "ubus",
1255
1328
  };
1256
1329
  }
1330
+ const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
1331
+ if (skillsMatch) {
1332
+ const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
1333
+ const action = String(args[0] || "list").toLowerCase();
1334
+ if (action === "list" || action === "ls") {
1335
+ const outcome = listUcodeSkills({ workspaceRoot });
1336
+ return {
1337
+ kind: "skills",
1338
+ output: formatSkillsList(outcome),
1339
+ skills: outcome.skills,
1340
+ errors: outcome.errors,
1341
+ };
1342
+ }
1343
+ if (action === "show") {
1344
+ const name = String(args[1] || "").trim();
1345
+ if (!name) {
1346
+ return {
1347
+ kind: "error",
1348
+ output: "usage: skills show <name>",
1349
+ };
1350
+ }
1351
+ const result = showSkill({ name, workspaceRoot });
1352
+ if (!result.ok) {
1353
+ return {
1354
+ kind: "error",
1355
+ output: result.error,
1356
+ };
1357
+ }
1358
+ return {
1359
+ kind: "skills",
1360
+ output: result.output,
1361
+ skill: result.skill,
1362
+ };
1363
+ }
1364
+ return {
1365
+ kind: "error",
1366
+ output: "usage: skills [list] | skills show <name>",
1367
+ };
1368
+ }
1257
1369
  if (text === "bg" || text === "/bg") {
1258
1370
  return {
1259
1371
  kind: "error",
@@ -1498,7 +1610,7 @@ async function runUcodeCoreAgent({
1498
1610
  if (result.kind === "probe") {
1499
1611
  return;
1500
1612
  }
1501
- if (result.kind === "help" || result.kind === "tool" || result.kind === "error") {
1613
+ if (result.kind === "help" || result.kind === "tool" || result.kind === "skills" || result.kind === "error") {
1502
1614
  stdout.write(`${result.output}\n`);
1503
1615
  }
1504
1616
  if (result.kind === "ubus") {
@@ -1701,6 +1813,8 @@ module.exports = {
1701
1813
  normalizeToolLogEvent,
1702
1814
  isProjectAnalysisTask,
1703
1815
  buildNlFallbackSummary,
1816
+ buildNlContext,
1817
+ stripSkillBlocksFromMessages,
1704
1818
  resolvePlannerProvider,
1705
1819
  extractJsonSummary,
1706
1820
  enrichNativeError,