@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/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
- return MODELS.find((m) => m.id === id);
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 = () => (session = sessions.newSession({ cwd: process.cwd(), model: state.model.id }));
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
- runSubAgent({
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: state.model.id,
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,