@noobdemon/noob-cli 1.7.7 → 1.7.10
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/package.json +1 -1
- package/skills/dynamic-workflows/SKILL.md +154 -0
- package/src/agent.js +101 -24
- package/src/api.js +157 -16
- package/src/i18n.js +35 -0
- package/src/models.js +14 -1
- package/src/repl.js +338 -9
- package/src/subagent.js +112 -53
- package/src/tokens.js +16 -0
- package/src/tools.js +46 -11
- package/src/workflows.js +142 -0
package/src/i18n.js
CHANGED
|
@@ -65,6 +65,9 @@ export const t = {
|
|
|
65
65
|
cmdFrontendDesign: "/frontend-design <yêu cầu> thiết kế UI frontend chất lượng cao theo skill (/fd)",
|
|
66
66
|
cmdImprove: "/improve [hint] phân tích workspace & đề xuất tính năng cải thiện (/imp)",
|
|
67
67
|
cmdUltra: "/ultra <mục tiêu> tự hành: noob tự nghĩ & tự làm nhiệm vụ tới khi xong (/u)",
|
|
68
|
+
cmdWorkflow: "/workflow <yêu cầu>|save|list|load|run|delete dynamic workflow đa sub-agent (/wf, /ultracode)",
|
|
69
|
+
cmdGoal: "/goal <text>|clear đặt HARD GOAL cho phiên (chống goal drift; không arg = xem)",
|
|
70
|
+
cmdLoop: "/loop <interval> <task> chạy task lặp lại (vd /loop 10m triage); /loop stop để dừng",
|
|
68
71
|
cmdLearn: "/learn [ghi chú] chưng cất bài học của phiên vào noob.md",
|
|
69
72
|
cmdCompact: "/compact tóm tắt phiên ngay để gọn ngữ cảnh (giữ trí nhớ dài hạn)",
|
|
70
73
|
cmdMemory: "/memory xem bộ nhớ noob.md (/mem)",
|
|
@@ -114,6 +117,18 @@ export const t = {
|
|
|
114
117
|
ultraMax: "Ultra: chạm giới hạn số vòng — dừng để bạn kiểm tra & ra lệnh tiếp.",
|
|
115
118
|
ultraNeedGoal: "Cần mục tiêu. Dùng: /ultra <mô tả mục tiêu>",
|
|
116
119
|
ultraQuest: (n) => `tự nghĩ nhiệm vụ kế tiếp (vòng ${n})…`,
|
|
120
|
+
loopNeedArgs: "Cần task. Dùng: /loop <interval> <task> (vd: /loop 5m kiểm tra log lỗi mới) · /loop stop để dừng · /loop để xem trạng thái",
|
|
121
|
+
loopBadInterval: (s) => `interval không hợp lệ: "${s}". Dùng dạng 30s / 5m / 1h / 2h30m.`,
|
|
122
|
+
loopStarted: (interval, task) => `🔁 Loop BẬT — chạy mỗi ${interval} (không giới hạn token): ${task}`,
|
|
123
|
+
// [GỠ BUDGET 2026-06-06] loopBadBudget + loopBudgetExceeded giữ lại để tương thích ngược nếu có code cũ gọi, không dùng nữa.
|
|
124
|
+
loopBadBudget: (s) => `(deprecated) ngân sách không còn được hỗ trợ: "${s}".`,
|
|
125
|
+
loopBudgetExceeded: (used, budget, ticks) => `(deprecated) loop không còn cap token.`,
|
|
126
|
+
loopStopped: "🔁 Loop đã dừng.",
|
|
127
|
+
loopNotRunning: "Không có loop nào đang chạy.",
|
|
128
|
+
loopStatus: (interval, task, ticks, nextIn) => `🔁 Loop: chạy mỗi ${interval} · đã ${ticks} lần · lần kế trong ~${nextIn}\n task: ${task}`,
|
|
129
|
+
loopTick: (n) => `🔁 loop tick #${n}…`,
|
|
130
|
+
loopAutoStop: (n) => `Loop tự dừng sau tick #${n} — model phát <<LOOP_DONE>> (task hoàn tất).`,
|
|
131
|
+
loopAlreadyRunning: "Đã có loop đang chạy. /loop stop trước khi đặt loop mới.",
|
|
117
132
|
learning: "đang chưng cất bài học vào noob.md…",
|
|
118
133
|
compactRunning: "đang tóm tắt phiên để gọn ngữ cảnh…",
|
|
119
134
|
compactEmpty: "Phiên còn trống — không có gì để tóm tắt.",
|
|
@@ -129,6 +144,26 @@ export const t = {
|
|
|
129
144
|
improveRunning: "đang khảo sát workspace & soạn đề xuất cải thiện…",
|
|
130
145
|
frontendDesignNoSkill: "không tìm thấy skills/frontend-design/SKILL.md — skill chưa được cài.",
|
|
131
146
|
frontendDesignNeedReq: "cần mô tả yêu cầu. Ví dụ: /frontend-design landing page cho app nghe nhạc lo-fi",
|
|
147
|
+
workflowRunning: "đang chạy dynamic workflow đa sub-agent…",
|
|
148
|
+
workflowNoSkill: "không tìm thấy skills/dynamic-workflows/SKILL.md — skill chưa được cài.",
|
|
149
|
+
workflowNeedArg: "cần mô tả task. Ví dụ: /workflow audit toàn bộ src/ tìm lỗ hổng SQL injection",
|
|
150
|
+
workflowAgentAutoOn: "agent mode tự bật cho /workflow (cần spawn_agent)",
|
|
151
|
+
// saved workflows (CRUD)
|
|
152
|
+
workflowListEmpty: (dir) => `Chưa có workflow đã lưu. Tạo bằng /workflow save <name> <yêu cầu>. Thư mục: ${dir}`,
|
|
153
|
+
workflowListHeader: (dir) => `Workflow đã lưu (${dir}):`,
|
|
154
|
+
workflowSaveNeedArgs: "Cách dùng: /workflow save <name> <yêu cầu workflow>",
|
|
155
|
+
workflowSaveBadName: (n) => `Tên workflow không hợp lệ: '${n}'. Chỉ chấp nhận [a-z0-9_-], bắt đầu bằng chữ/số, tối đa 64 ký tự.`,
|
|
156
|
+
workflowSaveError: (n, e) => `Không lưu được workflow '${n}': ${e}`,
|
|
157
|
+
workflowSaveOk: (n, p) => `Đã lưu workflow '${n}' → ${p}`,
|
|
158
|
+
workflowRunNeedName: "Cách dùng: /workflow run <name> [thêm ngữ cảnh]",
|
|
159
|
+
workflowRunError: (n, e) => `Không nạp được workflow '${n}': ${e}`,
|
|
160
|
+
workflowRunOk: (n) => `Chạy workflow đã lưu '${n}'…`,
|
|
161
|
+
workflowLoadNeedName: "Cách dùng: /workflow load <name>",
|
|
162
|
+
workflowLoadError: (n, e) => `Không nạp được workflow '${n}': ${e}`,
|
|
163
|
+
workflowLoadOk: (n, p) => `Workflow '${n}' (${p}):`,
|
|
164
|
+
workflowDeleteNeedName: "Cách dùng: /workflow delete <name>",
|
|
165
|
+
workflowDeleteError: (n, e) => `Không xoá được workflow '${n}': ${e}`,
|
|
166
|
+
workflowDeleteOk: (n) => `Đã xoá workflow '${n}'.`,
|
|
132
167
|
initOverwriteWarn: (p) => `⚠ Đã có noob.md tại ${p}. /init sẽ ghi đè nội dung hiện tại.`,
|
|
133
168
|
initOverwriteConfirm: "Ghi đè? gõ 'y' để xác nhận, phím khác để huỷ › ",
|
|
134
169
|
initCancel: "Huỷ /init — giữ nguyên noob.md.",
|
package/src/models.js
CHANGED
|
@@ -50,7 +50,20 @@ export const PROVIDERS = {
|
|
|
50
50
|
export const DEFAULT_MODEL = "gateway-claude-opus-4-7";
|
|
51
51
|
|
|
52
52
|
export function findModel(id) {
|
|
53
|
-
|
|
53
|
+
if (!id || typeof id !== "string") return undefined;
|
|
54
|
+
// Tier 1: exact id match (đường nhanh, dùng cho config + state nội bộ).
|
|
55
|
+
let hit = MODELS.find((m) => m.id === id);
|
|
56
|
+
if (hit) return hit;
|
|
57
|
+
// Tier 2: match mở rộng cho input từ model (sub-agent routing) hoặc user gõ tay.
|
|
58
|
+
// Bỏ prefix "gateway-", chuẩn hoá dấu (- _ . space → -), so id/name không phân biệt hoa thường.
|
|
59
|
+
const norm = (s) => String(s).toLowerCase().replace(/^gateway-/, "").replace(/[\s_.]+/g, "-").replace(/-+/g, "-");
|
|
60
|
+
const q = norm(id);
|
|
61
|
+
// Exact normalized match trước (tránh "opus" lỡ tay match nhầm "opus-4-1" khi muốn "opus-4-7").
|
|
62
|
+
hit = MODELS.find((m) => norm(m.id) === q || norm(m.name) === q);
|
|
63
|
+
if (hit) return hit;
|
|
64
|
+
// Tier 3: contains — chấp nhận khi CHỈ có 1 match duy nhất (tránh ambiguous).
|
|
65
|
+
const subs = MODELS.filter((m) => norm(m.id).includes(q) || norm(m.name).includes(q));
|
|
66
|
+
return subs.length === 1 ? subs[0] : undefined;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export function providerColor(providerKey) {
|
package/src/repl.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createTui } from "./tui.js";
|
|
|
6
6
|
import { runAgent, maybeSummarize } from "./agent.js";
|
|
7
7
|
import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from "./subagent.js";
|
|
8
8
|
import { TokenMeter } from "./tokens.js";
|
|
9
|
-
import { stream, usage, ApiError } from "./api.js";
|
|
9
|
+
import { stream, usage, ApiError, resetMemoryToken } from "./api.js";
|
|
10
10
|
import { runTool, describe, DESTRUCTIVE, addRoot, listRoots } from "./tools.js";
|
|
11
11
|
import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from "./models.js";
|
|
12
12
|
import { c, banner, modelBadge, renderMarkdown, box } from "./ui.js";
|
|
@@ -16,6 +16,7 @@ import { t } from "./i18n.js";
|
|
|
16
16
|
import { checkLatest, runUpdate, CURRENT } from "./update.js";
|
|
17
17
|
import * as sessions from "./sessions.js";
|
|
18
18
|
import { loadSkill, listSkills } from "./skills.js";
|
|
19
|
+
import { saveWorkflow, loadWorkflow, listWorkflows, deleteWorkflow, workflowsDir } from "./workflows.js";
|
|
19
20
|
|
|
20
21
|
// Lệnh dùng cho autocomplete. Gõ "/l" → lọc các lệnh có "l" (login, logout,
|
|
21
22
|
// clear, models, yolo…); ↑/↓ chọn, Tab điền, Enter chạy mục đang sáng.
|
|
@@ -31,9 +32,12 @@ const SLASH = [
|
|
|
31
32
|
{ name: "/init", desc: "quét dự án & tạo noob.md" },
|
|
32
33
|
{ name: "/karpathy", desc: "rà soát code (Karpathy)" },
|
|
33
34
|
{ name: "/frontend-design", desc: "thiết kế UI frontend chất lượng cao (skill)" },
|
|
35
|
+
{ name: "/workflow", desc: "orchestrate multi-agent dynamic workflow (skill)" },
|
|
34
36
|
{ name: "/improve", desc: "phân tích workspace & gợi ý tính năng cải thiện" },
|
|
35
37
|
{ name: "/ultra", desc: "tự hành: tự nghĩ & làm nhiệm vụ" },
|
|
36
38
|
{ name: "/agent", desc: "bật/tắt agent mode (spawn sub-agent)" },
|
|
39
|
+
{ name: "/goal", desc: "đặt HARD GOAL — model phải hướng tới tới khi /goal clear" },
|
|
40
|
+
{ name: "/loop", desc: "chạy task định kỳ (vd: /loop 5m kiểm tra log) · /loop stop để dừng" },
|
|
37
41
|
{ name: "/tokens", desc: "xem số token đã dùng phiên này" },
|
|
38
42
|
{ name: "/learn", desc: "chưng cất bài học vào noob.md" },
|
|
39
43
|
{ name: "/memory", desc: "xem bộ nhớ noob.md" },
|
|
@@ -136,6 +140,8 @@ export async function startRepl(opts = {}) {
|
|
|
136
140
|
yolo: !!opts.yolo || config.yoloDefault, // cờ --yolo HOẶC mặc định đã lưu (/auto-yolo)
|
|
137
141
|
ultra: false, // chế độ tự hành (self-quest) đang chạy?
|
|
138
142
|
agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
|
|
143
|
+
goal: null, // HARD GOAL (set qua /goal <text>) — inject vào mọi prompt tới khi /goal clear
|
|
144
|
+
loop: null, // /loop — {intervalMs, intervalStr, task, timer, ticks, startedAt} hoặc null
|
|
139
145
|
extraRoots: [], // thư mục ngoài cwd được /add-dir cấp quyền (UX display only;
|
|
140
146
|
// source of truth là extraRoots trong src/tools.js)
|
|
141
147
|
_longSessionWarned: false, // đã in cảnh báo phiên dài chưa (reset khi /clear)
|
|
@@ -278,16 +284,37 @@ export async function startRepl(opts = {}) {
|
|
|
278
284
|
if (!session || !state.history.length) return; // đừng lưu phiên rỗng
|
|
279
285
|
session.history = state.history; // giữ đồng bộ tuyệt đối với history sống
|
|
280
286
|
session.model = state.model.id;
|
|
287
|
+
session.goal = state.goal || null; // HARD GOAL bền qua --continue/--resume
|
|
288
|
+
session.tokens = tokenMeter.serialize(); // counter cộng dồn để hiển thị nhất quán qua resume
|
|
289
|
+
// state.loop: serialize toàn bộ trừ `timer` (Timeout object không JSON được).
|
|
290
|
+
if (state.loop) {
|
|
291
|
+
const { timer, running, ...loopSnap } = state.loop;
|
|
292
|
+
session.loop = loopSnap;
|
|
293
|
+
} else {
|
|
294
|
+
session.loop = null;
|
|
295
|
+
}
|
|
281
296
|
sessions.save(session);
|
|
282
297
|
};
|
|
283
298
|
async function restore(s) {
|
|
284
299
|
session = s;
|
|
285
300
|
state.history = s.history || [];
|
|
286
301
|
state.mode = "chat";
|
|
302
|
+
state.goal = s.goal || null; // khôi phục HARD GOAL nếu phiên cũ có
|
|
287
303
|
if (s.model) {
|
|
288
304
|
const m = findModel(s.model);
|
|
289
305
|
if (m) state.model = m;
|
|
290
306
|
}
|
|
307
|
+
// Re-arm /loop nếu phiên cũ đang chạy loop (timer/running không serialize được).
|
|
308
|
+
if (s.loop && s.loop.task && s.loop.intervalMs) {
|
|
309
|
+
state.loop = {
|
|
310
|
+
...s.loop,
|
|
311
|
+
running: false,
|
|
312
|
+
timer: null,
|
|
313
|
+
lastTickAt: Date.now(), // reset baseline để tick đầu chạy sau intervalMs
|
|
314
|
+
};
|
|
315
|
+
state.loop.timer = setInterval(makeLoopTick(s.loop.task), s.loop.intervalMs);
|
|
316
|
+
console.log(c.accent(" ↻ " + t.loopStatus(s.loop.intervalStr || fmtMs(s.loop.intervalMs), s.loop.task, s.loop.ticks || 0, fmtMs(s.loop.intervalMs))));
|
|
317
|
+
}
|
|
291
318
|
console.log(c.ok(" ✓ " + t.sessionResumed(s.id)));
|
|
292
319
|
const turns = state.history.filter((m) => m.role === "user");
|
|
293
320
|
const tail = turns.slice(-5);
|
|
@@ -333,7 +360,11 @@ export async function startRepl(opts = {}) {
|
|
|
333
360
|
);
|
|
334
361
|
console.log(c.dim("\n " + t.sessionResumeHint) + "\n");
|
|
335
362
|
}
|
|
336
|
-
const startFresh = () =>
|
|
363
|
+
const startFresh = () => {
|
|
364
|
+
session = sessions.newSession({ cwd: process.cwd(), model: state.model.id });
|
|
365
|
+
// Reset per-session upstream memory token so the next chat starts fresh.
|
|
366
|
+
resetMemoryToken();
|
|
367
|
+
};
|
|
337
368
|
|
|
338
369
|
// /frontend-design <yêu cầu> — vận dụng skill frontend-design (skills/frontend-design/SKILL.md)
|
|
339
370
|
// để model tạo UI frontend chất lượng cao, tránh "AI slop" aesthetic.
|
|
@@ -357,6 +388,118 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
357
388
|
persist();
|
|
358
389
|
}
|
|
359
390
|
|
|
391
|
+
// /workflow <yêu cầu> — chạy ad-hoc dynamic workflow
|
|
392
|
+
// /workflow save <name> <req> — lưu prompt template ra ~/.noob/workflows/<name>.md
|
|
393
|
+
// /workflow run <name> [extra] — chạy workflow đã lưu (extra context optional)
|
|
394
|
+
// /workflow load <name> — xem nội dung workflow đã lưu (không chạy)
|
|
395
|
+
// /workflow list — liệt kê workflow đã lưu
|
|
396
|
+
// /workflow delete <name> — xoá workflow đã lưu
|
|
397
|
+
// Cảm hứng tweet_dump.txt L183–193 ("saving and sharing dynamic workflows").
|
|
398
|
+
async function runWorkflow(arg) {
|
|
399
|
+
if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
|
|
400
|
+
if (!arg) return console.log(c.err(" " + (t.workflowNeedArg || "Cách dùng: /workflow <mô tả task lớn cần orchestrate>")));
|
|
401
|
+
// Detect sub-command. Sub-command tách bằng khoảng trắng đầu tiên.
|
|
402
|
+
const m = arg.match(/^(save|run|load|list|delete|rm|ls)\b\s*([\s\S]*)$/i);
|
|
403
|
+
if (m) {
|
|
404
|
+
const sub = m[1].toLowerCase();
|
|
405
|
+
const rest = m[2].trim();
|
|
406
|
+
if (sub === "list" || sub === "ls") return workflowList();
|
|
407
|
+
if (sub === "load") return workflowLoad(rest);
|
|
408
|
+
if (sub === "delete" || sub === "rm") return workflowDelete(rest);
|
|
409
|
+
if (sub === "save") return workflowSave(rest);
|
|
410
|
+
if (sub === "run") return workflowRun(rest);
|
|
411
|
+
}
|
|
412
|
+
// Default: ad-hoc workflow (giữ behavior cũ).
|
|
413
|
+
await workflowExecute(arg);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Chạy thật workflow prompt — chia sẻ giữa ad-hoc và `run <name>`.
|
|
417
|
+
async function workflowExecute(userRequest) {
|
|
418
|
+
const skill = loadSkill("dynamic-workflows");
|
|
419
|
+
if (!skill) return console.log(c.err(" " + (t.workflowNoSkill || "Không tìm thấy skill dynamic-workflows")));
|
|
420
|
+
if (!state.agent) {
|
|
421
|
+
state.agent = true;
|
|
422
|
+
console.log(c.tool(" " + (t.workflowAgentAutoOn || "agent mode tự bật cho /workflow")));
|
|
423
|
+
}
|
|
424
|
+
const prompt = `Bạn đang thực thi SKILL "dynamic-workflows". Đọc kỹ playbook dưới đây và TUÂN THỦ khi orchestrate multi-agent workflow.
|
|
425
|
+
|
|
426
|
+
=== SKILL: dynamic-workflows ===
|
|
427
|
+
${skill}
|
|
428
|
+
=== HẾT SKILL ===
|
|
429
|
+
|
|
430
|
+
YÊU CẦU NGƯỜI DÙNG:
|
|
431
|
+
${userRequest}
|
|
432
|
+
|
|
433
|
+
Thực thi:
|
|
434
|
+
1. Viết PLAN ngắn (sub-tasks, sequential vs parallel, synthesis step, stop condition).
|
|
435
|
+
2. Spawn sub-agent theo plan — dùng spawn_agents cho công việc song song độc lập, spawn_agent tuần tự khi có phụ thuộc.
|
|
436
|
+
3. Mỗi prompt sub-agent đều có GOAL / INPUTS / METHOD / OUTPUT SHAPE / STOP CONDITION (theo skill).
|
|
437
|
+
4. Gom kết quả, dedupe, reconcile xung đột, viết báo cáo cuối tiếng Việt cho người dùng. Sub-agent KHÔNG nói trực tiếp với user.`;
|
|
438
|
+
console.log(c.tool(" 🎼 " + (t.workflowRunning || "Dynamic workflow running…")));
|
|
439
|
+
await handle(prompt);
|
|
440
|
+
persist();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function workflowList() {
|
|
444
|
+
const items = listWorkflows();
|
|
445
|
+
if (!items.length) {
|
|
446
|
+
console.log(c.dim(" " + (t.workflowListEmpty ? t.workflowListEmpty(workflowsDir()) : `Chưa có workflow đã lưu. Tạo bằng /workflow save <name> <yêu cầu>. Thư mục: ${workflowsDir()}`)));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log(c.tool(" " + (t.workflowListHeader ? t.workflowListHeader(workflowsDir()) : `Workflow đã lưu (${workflowsDir()}):`)));
|
|
450
|
+
for (const it of items) {
|
|
451
|
+
const desc = it.description ? c.dim(" — " + it.description) : "";
|
|
452
|
+
const date = it.updated ? c.dim(" [" + it.updated.slice(0, 10) + "]") : "";
|
|
453
|
+
console.log(" " + c.accent(it.name) + desc + date);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function workflowLoad(name) {
|
|
458
|
+
if (!name) return console.log(c.err(" " + (t.workflowLoadNeedName || "Cách dùng: /workflow load <name>")));
|
|
459
|
+
const r = loadWorkflow(name);
|
|
460
|
+
if (!r.ok) return console.log(c.err(" " + (t.workflowLoadError ? t.workflowLoadError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
|
|
461
|
+
console.log(c.tool(" " + (t.workflowLoadOk ? t.workflowLoadOk(r.name, r.path) : `Workflow '${r.name}' (${r.path}):`)));
|
|
462
|
+
if (r.meta.description) console.log(c.dim(" " + r.meta.description));
|
|
463
|
+
if (r.meta.updated) console.log(c.dim(" updated: " + r.meta.updated));
|
|
464
|
+
console.log("");
|
|
465
|
+
console.log(r.prompt);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function workflowDelete(name) {
|
|
469
|
+
if (!name) return console.log(c.err(" " + (t.workflowDeleteNeedName || "Cách dùng: /workflow delete <name>")));
|
|
470
|
+
const r = deleteWorkflow(name);
|
|
471
|
+
if (!r.ok) return console.log(c.err(" " + (t.workflowDeleteError ? t.workflowDeleteError(name, r.error) : `Không xoá được workflow '${name}': ${r.error}`)));
|
|
472
|
+
console.log(c.tool(" " + (t.workflowDeleteOk ? t.workflowDeleteOk(name) : `Đã xoá workflow '${name}'.`)));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function workflowSave(rest) {
|
|
476
|
+
// /workflow save <name> <yêu cầu...>
|
|
477
|
+
const m = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
478
|
+
if (!m) return console.log(c.err(" " + (t.workflowSaveNeedArgs || "Cách dùng: /workflow save <name> <yêu cầu workflow>")));
|
|
479
|
+
const name = m[1];
|
|
480
|
+
const prompt = m[2].trim();
|
|
481
|
+
const r = saveWorkflow(name, prompt);
|
|
482
|
+
if (!r.ok) {
|
|
483
|
+
const msg = r.error === "invalid_name"
|
|
484
|
+
? (t.workflowSaveBadName ? t.workflowSaveBadName(name) : `Tên workflow không hợp lệ: '${name}'. Chỉ chấp nhận [a-z0-9_-], bắt đầu bằng chữ/số, tối đa 64 ký tự.`)
|
|
485
|
+
: (t.workflowSaveError ? t.workflowSaveError(name, r.error) : `Không lưu được workflow '${name}': ${r.error}`);
|
|
486
|
+
return console.log(c.err(" " + msg));
|
|
487
|
+
}
|
|
488
|
+
console.log(c.tool(" 💾 " + (t.workflowSaveOk ? t.workflowSaveOk(name, r.path) : `Đã lưu workflow '${name}' → ${r.path}`)));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function workflowRun(rest) {
|
|
492
|
+
if (!rest) return console.log(c.err(" " + (t.workflowRunNeedName || "Cách dùng: /workflow run <name> [thêm ngữ cảnh]")));
|
|
493
|
+
const m = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
494
|
+
const name = m[1];
|
|
495
|
+
const extra = (m[2] || "").trim();
|
|
496
|
+
const r = loadWorkflow(name);
|
|
497
|
+
if (!r.ok) return console.log(c.err(" " + (t.workflowRunError ? t.workflowRunError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
|
|
498
|
+
const userRequest = extra ? `${r.prompt}\n\nNgữ cảnh bổ sung cho lần chạy này:\n${extra}` : r.prompt;
|
|
499
|
+
console.log(c.tool(" ▶️ " + (t.workflowRunOk ? t.workflowRunOk(name) : `Chạy workflow đã lưu '${name}'…`)));
|
|
500
|
+
await workflowExecute(userRequest);
|
|
501
|
+
}
|
|
502
|
+
|
|
360
503
|
// /improve [hint] — model rà soát workspace & đề xuất tính năng/cải tiến.
|
|
361
504
|
// KHÔNG sửa code, chỉ phân tích & đề xuất.
|
|
362
505
|
async function runImprove(arg) {
|
|
@@ -396,6 +539,20 @@ Cuối cùng: tóm tắt + danh sách sửa theo thứ tự ưu tiên. Thẳng t
|
|
|
396
539
|
// bắt nhầm khi model chỉ NHẮC tới token giữa văn xuôi. Không bao giờ chấp nhận
|
|
397
540
|
// ở lượt lập kế hoạch (xem vòng lặp bên dưới).
|
|
398
541
|
const ultraIsDone = (a) => a.trimEnd().endsWith(ULTRA_DONE);
|
|
542
|
+
// Detect "stuck": model bối rối, không nhận task, chỉ hỏi lại user hoặc spam
|
|
543
|
+
// list_dir/ls vô nghĩa. Xảy ra khi goal trống nghĩa / bị paste system prompt /
|
|
544
|
+
// model mất ngữ cảnh. 2 vòng stuck liên tiếp → auto-exit để không loop vô hạn.
|
|
545
|
+
const STUCK_PHRASES = [
|
|
546
|
+
"chưa giao task", "chưa nêu tác vụ", "chưa có yêu cầu", "chưa có task",
|
|
547
|
+
"không nhận task", "không thể nhận vai", "bạn muốn mình làm gì",
|
|
548
|
+
"chưa rõ yêu cầu", "cần mục tiêu rõ", "vui lòng cho biết",
|
|
549
|
+
"please provide", "what would you like", "no task", "clarify",
|
|
550
|
+
];
|
|
551
|
+
const ultraLooksStuck = (a) => {
|
|
552
|
+
if (!a) return true;
|
|
553
|
+
const s = a.toLowerCase();
|
|
554
|
+
return STUCK_PHRASES.some((p) => s.includes(p));
|
|
555
|
+
};
|
|
399
556
|
const ultraStart = (goal) => `# CHẾ ĐỘ ULTRA (tự hành)
|
|
400
557
|
Mục tiêu tổng: ${goal}
|
|
401
558
|
|
|
@@ -429,10 +586,23 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
429
586
|
let answer = await handle(ultraStart(goal));
|
|
430
587
|
persist();
|
|
431
588
|
let i = 0;
|
|
589
|
+
let stuckStreak = 0; // đếm vòng liên tiếp model bối rối / không nhận task
|
|
590
|
+
const STUCK_MAX = 2;
|
|
432
591
|
// Lượt đầu = lập kế hoạch → KHÔNG xét hoàn thành. Mỗi vòng sau là một lượt
|
|
433
592
|
// "tiếp tục" có cổng kiểm chứng; chỉ dừng khi token nằm ở CUỐI câu trả lời.
|
|
593
|
+
// Cũng dừng sớm nếu model 2 vòng liên tiếp tỏ ra mất ngữ cảnh (goal trống
|
|
594
|
+
// nghĩa, paste system prompt, model hỏi lại user) → tránh spam list_dir.
|
|
434
595
|
while (state.ultra && i < MAX_QUESTS) {
|
|
435
596
|
if (!answer) break; // lượt bị ngắt/ lỗi → dừng tự hành, đừng quay vô ích
|
|
597
|
+
if (ultraLooksStuck(answer)) {
|
|
598
|
+
stuckStreak++;
|
|
599
|
+
if (stuckStreak >= STUCK_MAX) {
|
|
600
|
+
console.log(c.err(" ⚠ ULTRA stuck: model không nhận task " + stuckStreak + " vòng liên tiếp. Thoát. Gõ /ultra <mục tiêu rõ> để thử lại."));
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
stuckStreak = 0;
|
|
605
|
+
}
|
|
436
606
|
i++;
|
|
437
607
|
console.log(c.accent(" ↻ " + t.ultraQuest(i)));
|
|
438
608
|
answer = await handle(ultraContinue(goal));
|
|
@@ -458,6 +628,125 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
458
628
|
}
|
|
459
629
|
}
|
|
460
630
|
|
|
631
|
+
// /loop — chạy task định kỳ (tweet_dump.txt: "Pair triage workflows with /loop
|
|
632
|
+
// to have Claude do this continuously" + "be run at regular intervals"). Combo
|
|
633
|
+
// tự nhiên với /goal (hard completion req) — gõ /goal trước, rồi /loop để chạy
|
|
634
|
+
// task tới khi goal đạt qua từng tick.
|
|
635
|
+
// Cú pháp: /loop <interval> <task> (vd: /loop 5m kiểm tra log lỗi mới)
|
|
636
|
+
// /loop stop — dừng loop hiện tại
|
|
637
|
+
// /loop — xem trạng thái
|
|
638
|
+
// Interval parser: 30s / 5m / 1h / 2h30m / 90s — số + đơn vị (s/m/h), nối được.
|
|
639
|
+
function parseInterval(s) {
|
|
640
|
+
if (!s) return null;
|
|
641
|
+
const re = /(\d+)\s*(h|m|s)/gi;
|
|
642
|
+
let total = 0, matched = false, m;
|
|
643
|
+
while ((m = re.exec(s)) !== null) {
|
|
644
|
+
matched = true;
|
|
645
|
+
const n = parseInt(m[1], 10);
|
|
646
|
+
const u = m[2].toLowerCase();
|
|
647
|
+
if (u === "h") total += n * 3600_000;
|
|
648
|
+
else if (u === "m") total += n * 60_000;
|
|
649
|
+
else total += n * 1000;
|
|
650
|
+
}
|
|
651
|
+
if (!matched || total < 5000) return null; // tối thiểu 5s — tránh hammer
|
|
652
|
+
return total;
|
|
653
|
+
}
|
|
654
|
+
function fmtMs(ms) {
|
|
655
|
+
if (ms < 60_000) return Math.round(ms / 1000) + "s";
|
|
656
|
+
if (ms < 3600_000) {
|
|
657
|
+
const m = Math.floor(ms / 60_000), s = Math.round((ms % 60_000) / 1000);
|
|
658
|
+
return s ? `${m}m${s}s` : `${m}m`;
|
|
659
|
+
}
|
|
660
|
+
const h = Math.floor(ms / 3600_000), mm = Math.round((ms % 3600_000) / 60_000);
|
|
661
|
+
return mm ? `${h}h${mm}m` : `${h}h`;
|
|
662
|
+
}
|
|
663
|
+
function stopLoop() {
|
|
664
|
+
if (!state.loop) return false;
|
|
665
|
+
clearInterval(state.loop.timer);
|
|
666
|
+
state.loop = null;
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
// Factory tạo tick handler — tách ra để cả runLoop và restore() dùng chung.
|
|
670
|
+
// Capture task qua tham số (không phải closure scope) → re-arm được sau resume.
|
|
671
|
+
function makeLoopTick(task) {
|
|
672
|
+
return async () => {
|
|
673
|
+
if (!state.loop) return; // đã bị stop
|
|
674
|
+
if (state.loop.running) return; // tick trước còn chạy → skip lượt này
|
|
675
|
+
state.loop.running = true;
|
|
676
|
+
state.loop.ticks++;
|
|
677
|
+
state.loop.lastTickAt = Date.now();
|
|
678
|
+
try {
|
|
679
|
+
console.log(c.dim(" " + t.loopTick(state.loop.ticks)));
|
|
680
|
+
const answer = await handle(loopTickPrompt(task, state.loop.ticks));
|
|
681
|
+
persist();
|
|
682
|
+
if (loopIsDone(answer)) {
|
|
683
|
+
console.log(c.ok(" ✓ " + t.loopAutoStop(state.loop.ticks)));
|
|
684
|
+
stopLoop();
|
|
685
|
+
}
|
|
686
|
+
// [GỠ BUDGET 2026-06-06] Không còn cap token cho /loop — loop dừng theo
|
|
687
|
+
// <<LOOP_DONE>> hoặc /loop stop, không bị cắt giữa chừng vì "hết token".
|
|
688
|
+
} catch (e) {
|
|
689
|
+
console.log(c.err(" loop tick lỗi: " + (e?.message || e)));
|
|
690
|
+
} finally {
|
|
691
|
+
if (state.loop) state.loop.running = false;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
// /loop auto-stop: model phát token <<LOOP_DONE>> ở CUỐI reply khi thấy không
|
|
696
|
+
// còn việc / goal đã đạt → loop tự dừng. Combo tự nhiên với /goal: gõ /goal
|
|
697
|
+
// trước rồi /loop, model tự đánh giá goal mỗi tick và phát LOOP_DONE khi đủ.
|
|
698
|
+
const LOOP_DONE = "<<LOOP_DONE>>";
|
|
699
|
+
const loopIsDone = (a) => a && a.trimEnd().endsWith(LOOP_DONE);
|
|
700
|
+
const loopTickPrompt = (task, n) => `[LOOP tick #${n}] ${task}
|
|
701
|
+
|
|
702
|
+
Đây là tick định kỳ trong chế độ /loop. Làm việc bình thường — đọc/sửa file, chạy lệnh nếu cần.
|
|
703
|
+
Sau khi xong tick này, TỰ ĐÁNH GIÁ:
|
|
704
|
+
- Nếu còn việc / điều kiện chưa đạt → trả lời bình thường, KHÔNG phát token.
|
|
705
|
+
- Nếu task này đã hoàn tất hẳn (mọi điều cần làm đều đã làm, hoặc goal nếu có đã đạt) và không còn lý do để tick tiếp → đặt token ${LOOP_DONE} TRÊN MỘT DÒNG RIÊNG ở CUỐI reply để dừng loop.`;
|
|
706
|
+
|
|
707
|
+
async function runLoop(arg) {
|
|
708
|
+
const a = (arg || "").trim();
|
|
709
|
+
// /loop (no arg) → status
|
|
710
|
+
if (!a) {
|
|
711
|
+
if (!state.loop) return console.log(c.dim(" " + t.loopNotRunning) + c.dim(" " + t.loopNeedArgs));
|
|
712
|
+
const L = state.loop;
|
|
713
|
+
const elapsed = Date.now() - L.lastTickAt;
|
|
714
|
+
const nextIn = Math.max(0, L.intervalMs - elapsed);
|
|
715
|
+
return console.log(c.accent(" " + t.loopStatus(L.intervalStr, L.task, L.ticks, fmtMs(nextIn))));
|
|
716
|
+
}
|
|
717
|
+
// /loop stop
|
|
718
|
+
if (/^(stop|off|dừng|dung|tắt|tat)$/i.test(a)) {
|
|
719
|
+
if (stopLoop()) console.log(c.ok(" " + t.loopStopped));
|
|
720
|
+
else console.log(c.dim(" " + t.loopNotRunning));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (state.loop) return console.log(c.err(" " + t.loopAlreadyRunning));
|
|
724
|
+
if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
|
|
725
|
+
// parse <interval> <task>
|
|
726
|
+
// [GỠ BUDGET 2026-06-06] Cú pháp đơn giản: /loop <interval> <task>. Không còn cap token.
|
|
727
|
+
const firstSpace = a.search(/\s/);
|
|
728
|
+
if (firstSpace < 0) return console.log(c.err(" " + t.loopNeedArgs));
|
|
729
|
+
const intervalStr = a.slice(0, firstSpace).trim();
|
|
730
|
+
const task = a.slice(firstSpace + 1).trim();
|
|
731
|
+
if (!task) return console.log(c.err(" " + t.loopNeedArgs));
|
|
732
|
+
const intervalMs = parseInterval(intervalStr);
|
|
733
|
+
if (!intervalMs) return console.log(c.err(" " + t.loopBadInterval(intervalStr)));
|
|
734
|
+
const normInterval = fmtMs(intervalMs);
|
|
735
|
+
state.loop = {
|
|
736
|
+
intervalMs,
|
|
737
|
+
intervalStr: normInterval,
|
|
738
|
+
task,
|
|
739
|
+
ticks: 0,
|
|
740
|
+
startedAt: Date.now(),
|
|
741
|
+
lastTickAt: Date.now(),
|
|
742
|
+
running: false, // chống re-entrant (tick trước chưa xong, tick sau tới)
|
|
743
|
+
timer: null,
|
|
744
|
+
};
|
|
745
|
+
console.log(c.accent(" " + t.loopStarted(normInterval, task)));
|
|
746
|
+
state.loop.timer = setInterval(makeLoopTick(task), intervalMs);
|
|
747
|
+
// KHÔNG tick ngay — user có thể muốn gõ thêm lệnh khác trước khi tick đầu chạy.
|
|
748
|
+
}
|
|
749
|
+
|
|
461
750
|
// /init — quét dự án & sinh noob.md tổng quan (giống `/init` của Claude Code).
|
|
462
751
|
// Nếu noob.md đã có: hỏi xác nhận ghi đè trước khi giao việc cho model.
|
|
463
752
|
async function runInit() {
|
|
@@ -748,18 +1037,28 @@ NGUYÊN TẮC:
|
|
|
748
1037
|
console.log(chalk.hex("#8b5cf6")(` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`));
|
|
749
1038
|
startSpin(t.thinking);
|
|
750
1039
|
try {
|
|
751
|
-
const results = await Promise.all(tasks.map((task, i) =>
|
|
752
|
-
|
|
1040
|
+
const results = await Promise.all(tasks.map((task, i) => {
|
|
1041
|
+
// Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
|
|
1042
|
+
// findModel() resolve cả hai; nếu không match thì fallback model của cha.
|
|
1043
|
+
let subModel = state.model.id;
|
|
1044
|
+
let modelTag = "";
|
|
1045
|
+
if (task?.model) {
|
|
1046
|
+
const m = findModel(task.model);
|
|
1047
|
+
if (m) { subModel = m.id; modelTag = ` [${m.name}]`; }
|
|
1048
|
+
else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
1049
|
+
}
|
|
1050
|
+
// [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
|
|
1051
|
+
return runSubAgent({
|
|
753
1052
|
task: task?.task || task?.prompt || "",
|
|
754
1053
|
context: task?.context || "",
|
|
755
1054
|
depth: depth + 1,
|
|
756
|
-
model:
|
|
1055
|
+
model: subModel,
|
|
757
1056
|
signal: abort.signal,
|
|
758
1057
|
tokenMeter,
|
|
759
1058
|
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
760
|
-
onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg)); startSpin(t.thinking); },
|
|
761
|
-
}).then((r) => `── sub-agent #${i + 1} ──\n${r}`).catch((e) => `── sub-agent #${i + 1} (LỖI) ──\n${e?.message || String(e)}`)
|
|
762
|
-
));
|
|
1059
|
+
onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg + modelTag)); startSpin(t.thinking); },
|
|
1060
|
+
}).then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`).catch((e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`);
|
|
1061
|
+
}));
|
|
763
1062
|
return { allow: true, result: results.join("\n\n") };
|
|
764
1063
|
} catch (err) {
|
|
765
1064
|
return { allow: true, result: "ERROR sub-agent: " + (err?.message || String(err)) };
|
|
@@ -776,6 +1075,7 @@ NGUYÊN TẮC:
|
|
|
776
1075
|
model: state.model.id,
|
|
777
1076
|
signal: abort.signal,
|
|
778
1077
|
tokenMeter,
|
|
1078
|
+
goal: state.goal,
|
|
779
1079
|
extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
|
|
780
1080
|
onStatus: () => tick(t.thinking),
|
|
781
1081
|
onSteer: () => {
|
|
@@ -884,7 +1184,7 @@ NGUYÊN TẮC:
|
|
|
884
1184
|
|
|
885
1185
|
tui.status(c.dim(" " + t.running));
|
|
886
1186
|
try {
|
|
887
|
-
const result = await runTool(name, input);
|
|
1187
|
+
const result = await runTool(name, input, { signal: abort?.signal });
|
|
888
1188
|
tui.status(null);
|
|
889
1189
|
console.log(c.ok(" ✓ ") + c.dim(firstLine(result)));
|
|
890
1190
|
return { allow: true, result };
|
|
@@ -964,6 +1264,24 @@ NGUYÊN TẮC:
|
|
|
964
1264
|
console.log((state.agentMode ? c.accent : c.dim)(" agent mode: " + (state.agentMode ? "BẬT (spawn_agent / spawn_agents khả dụng, depth tối đa " + MAX_SUBAGENT_DEPTH + ")" : "tắt")));
|
|
965
1265
|
break;
|
|
966
1266
|
}
|
|
1267
|
+
case "goal": {
|
|
1268
|
+
// HARD GOAL = completion requirement (xem tweet_dump.txt mục "Combine
|
|
1269
|
+
// with /goal and /loop"). Set xong sẽ inject vào MỌI prompt tới khi clear.
|
|
1270
|
+
const v = arg.trim();
|
|
1271
|
+
if (!v) {
|
|
1272
|
+
if (state.goal) console.log(c.accent(" 🎯 goal: ") + state.goal);
|
|
1273
|
+
else console.log(c.dim(" chưa đặt goal. Cú pháp: /goal <mục tiêu> · /goal clear để xoá"));
|
|
1274
|
+
} else if (v.toLowerCase() === "clear" || v.toLowerCase() === "off" || v.toLowerCase() === "xoá" || v.toLowerCase() === "xoa") {
|
|
1275
|
+
state.goal = null;
|
|
1276
|
+
console.log(c.dim(" đã xoá goal"));
|
|
1277
|
+
persist();
|
|
1278
|
+
} else {
|
|
1279
|
+
state.goal = v;
|
|
1280
|
+
console.log(c.accent(" 🎯 đã đặt goal: ") + v);
|
|
1281
|
+
persist();
|
|
1282
|
+
}
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
967
1285
|
case "tokens": {
|
|
968
1286
|
console.log(c.dim(` tokens — input: ${tokenMeter.input.toLocaleString("vi-VN")} · output: ${tokenMeter.output.toLocaleString("vi-VN")} · tổng: ${tokenMeter.total.toLocaleString("vi-VN")} · ${tokenMeter.format()}`));
|
|
969
1287
|
break;
|
|
@@ -982,6 +1300,11 @@ NGUYÊN TẮC:
|
|
|
982
1300
|
case "fd":
|
|
983
1301
|
await runFrontendDesign(arg);
|
|
984
1302
|
break;
|
|
1303
|
+
case "workflow":
|
|
1304
|
+
case "wf":
|
|
1305
|
+
case "ultracode":
|
|
1306
|
+
await runWorkflow(arg);
|
|
1307
|
+
break;
|
|
985
1308
|
case "improve":
|
|
986
1309
|
case "imp":
|
|
987
1310
|
await runImprove(arg);
|
|
@@ -990,6 +1313,9 @@ NGUYÊN TẮC:
|
|
|
990
1313
|
case "u":
|
|
991
1314
|
await runUltra(arg);
|
|
992
1315
|
break;
|
|
1316
|
+
case "loop":
|
|
1317
|
+
await runLoop(arg);
|
|
1318
|
+
break;
|
|
993
1319
|
case "init":
|
|
994
1320
|
await runInit();
|
|
995
1321
|
break;
|
|
@@ -1264,6 +1590,9 @@ function printHelp() {
|
|
|
1264
1590
|
" " + t.cmdFrontendDesign,
|
|
1265
1591
|
" " + t.cmdImprove,
|
|
1266
1592
|
" " + t.cmdUltra,
|
|
1593
|
+
" " + t.cmdWorkflow,
|
|
1594
|
+
" " + t.cmdGoal,
|
|
1595
|
+
" " + t.cmdLoop,
|
|
1267
1596
|
" " + t.cmdLearn,
|
|
1268
1597
|
" " + t.cmdCompact,
|
|
1269
1598
|
" " + t.cmdMemory,
|