@noobdemon/noob-cli 1.7.6 → 1.7.8

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/repl.js CHANGED
@@ -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);
@@ -357,6 +384,118 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
357
384
  persist();
358
385
  }
359
386
 
387
+ // /workflow <yêu cầu> — chạy ad-hoc dynamic workflow
388
+ // /workflow save <name> <req> — lưu prompt template ra ~/.noob/workflows/<name>.md
389
+ // /workflow run <name> [extra] — chạy workflow đã lưu (extra context optional)
390
+ // /workflow load <name> — xem nội dung workflow đã lưu (không chạy)
391
+ // /workflow list — liệt kê workflow đã lưu
392
+ // /workflow delete <name> — xoá workflow đã lưu
393
+ // Cảm hứng tweet_dump.txt L183–193 ("saving and sharing dynamic workflows").
394
+ async function runWorkflow(arg) {
395
+ if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
396
+ if (!arg) return console.log(c.err(" " + (t.workflowNeedArg || "Cách dùng: /workflow <mô tả task lớn cần orchestrate>")));
397
+ // Detect sub-command. Sub-command tách bằng khoảng trắng đầu tiên.
398
+ const m = arg.match(/^(save|run|load|list|delete|rm|ls)\b\s*([\s\S]*)$/i);
399
+ if (m) {
400
+ const sub = m[1].toLowerCase();
401
+ const rest = m[2].trim();
402
+ if (sub === "list" || sub === "ls") return workflowList();
403
+ if (sub === "load") return workflowLoad(rest);
404
+ if (sub === "delete" || sub === "rm") return workflowDelete(rest);
405
+ if (sub === "save") return workflowSave(rest);
406
+ if (sub === "run") return workflowRun(rest);
407
+ }
408
+ // Default: ad-hoc workflow (giữ behavior cũ).
409
+ await workflowExecute(arg);
410
+ }
411
+
412
+ // Chạy thật workflow prompt — chia sẻ giữa ad-hoc và `run <name>`.
413
+ async function workflowExecute(userRequest) {
414
+ const skill = loadSkill("dynamic-workflows");
415
+ if (!skill) return console.log(c.err(" " + (t.workflowNoSkill || "Không tìm thấy skill dynamic-workflows")));
416
+ if (!state.agent) {
417
+ state.agent = true;
418
+ console.log(c.tool(" " + (t.workflowAgentAutoOn || "agent mode tự bật cho /workflow")));
419
+ }
420
+ 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.
421
+
422
+ === SKILL: dynamic-workflows ===
423
+ ${skill}
424
+ === HẾT SKILL ===
425
+
426
+ YÊU CẦU NGƯỜI DÙNG:
427
+ ${userRequest}
428
+
429
+ Thực thi:
430
+ 1. Viết PLAN ngắn (sub-tasks, sequential vs parallel, synthesis step, stop condition).
431
+ 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.
432
+ 3. Mỗi prompt sub-agent đều có GOAL / INPUTS / METHOD / OUTPUT SHAPE / STOP CONDITION (theo skill).
433
+ 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.`;
434
+ console.log(c.tool(" 🎼 " + (t.workflowRunning || "Dynamic workflow running…")));
435
+ await handle(prompt);
436
+ persist();
437
+ }
438
+
439
+ function workflowList() {
440
+ const items = listWorkflows();
441
+ if (!items.length) {
442
+ 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()}`)));
443
+ return;
444
+ }
445
+ console.log(c.tool(" " + (t.workflowListHeader ? t.workflowListHeader(workflowsDir()) : `Workflow đã lưu (${workflowsDir()}):`)));
446
+ for (const it of items) {
447
+ const desc = it.description ? c.dim(" — " + it.description) : "";
448
+ const date = it.updated ? c.dim(" [" + it.updated.slice(0, 10) + "]") : "";
449
+ console.log(" " + c.accent(it.name) + desc + date);
450
+ }
451
+ }
452
+
453
+ function workflowLoad(name) {
454
+ if (!name) return console.log(c.err(" " + (t.workflowLoadNeedName || "Cách dùng: /workflow load <name>")));
455
+ const r = loadWorkflow(name);
456
+ if (!r.ok) return console.log(c.err(" " + (t.workflowLoadError ? t.workflowLoadError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
457
+ console.log(c.tool(" " + (t.workflowLoadOk ? t.workflowLoadOk(r.name, r.path) : `Workflow '${r.name}' (${r.path}):`)));
458
+ if (r.meta.description) console.log(c.dim(" " + r.meta.description));
459
+ if (r.meta.updated) console.log(c.dim(" updated: " + r.meta.updated));
460
+ console.log("");
461
+ console.log(r.prompt);
462
+ }
463
+
464
+ function workflowDelete(name) {
465
+ if (!name) return console.log(c.err(" " + (t.workflowDeleteNeedName || "Cách dùng: /workflow delete <name>")));
466
+ const r = deleteWorkflow(name);
467
+ if (!r.ok) return console.log(c.err(" " + (t.workflowDeleteError ? t.workflowDeleteError(name, r.error) : `Không xoá được workflow '${name}': ${r.error}`)));
468
+ console.log(c.tool(" " + (t.workflowDeleteOk ? t.workflowDeleteOk(name) : `Đã xoá workflow '${name}'.`)));
469
+ }
470
+
471
+ function workflowSave(rest) {
472
+ // /workflow save <name> <yêu cầu...>
473
+ const m = rest.match(/^(\S+)\s+([\s\S]+)$/);
474
+ if (!m) return console.log(c.err(" " + (t.workflowSaveNeedArgs || "Cách dùng: /workflow save <name> <yêu cầu workflow>")));
475
+ const name = m[1];
476
+ const prompt = m[2].trim();
477
+ const r = saveWorkflow(name, prompt);
478
+ if (!r.ok) {
479
+ const msg = r.error === "invalid_name"
480
+ ? (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ự.`)
481
+ : (t.workflowSaveError ? t.workflowSaveError(name, r.error) : `Không lưu được workflow '${name}': ${r.error}`);
482
+ return console.log(c.err(" " + msg));
483
+ }
484
+ console.log(c.tool(" 💾 " + (t.workflowSaveOk ? t.workflowSaveOk(name, r.path) : `Đã lưu workflow '${name}' → ${r.path}`)));
485
+ }
486
+
487
+ async function workflowRun(rest) {
488
+ if (!rest) return console.log(c.err(" " + (t.workflowRunNeedName || "Cách dùng: /workflow run <name> [thêm ngữ cảnh]")));
489
+ const m = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
490
+ const name = m[1];
491
+ const extra = (m[2] || "").trim();
492
+ const r = loadWorkflow(name);
493
+ if (!r.ok) return console.log(c.err(" " + (t.workflowRunError ? t.workflowRunError(name, r.error) : `Không nạp được workflow '${name}': ${r.error}`)));
494
+ const userRequest = extra ? `${r.prompt}\n\nNgữ cảnh bổ sung cho lần chạy này:\n${extra}` : r.prompt;
495
+ console.log(c.tool(" ▶️ " + (t.workflowRunOk ? t.workflowRunOk(name) : `Chạy workflow đã lưu '${name}'…`)));
496
+ await workflowExecute(userRequest);
497
+ }
498
+
360
499
  // /improve [hint] — model rà soát workspace & đề xuất tính năng/cải tiến.
361
500
  // KHÔNG sửa code, chỉ phân tích & đề xuất.
362
501
  async function runImprove(arg) {
@@ -458,6 +597,125 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
458
597
  }
459
598
  }
460
599
 
600
+ // /loop — chạy task định kỳ (tweet_dump.txt: "Pair triage workflows with /loop
601
+ // to have Claude do this continuously" + "be run at regular intervals"). Combo
602
+ // tự nhiên với /goal (hard completion req) — gõ /goal trước, rồi /loop để chạy
603
+ // task tới khi goal đạt qua từng tick.
604
+ // Cú pháp: /loop <interval> <task> (vd: /loop 5m kiểm tra log lỗi mới)
605
+ // /loop stop — dừng loop hiện tại
606
+ // /loop — xem trạng thái
607
+ // Interval parser: 30s / 5m / 1h / 2h30m / 90s — số + đơn vị (s/m/h), nối được.
608
+ function parseInterval(s) {
609
+ if (!s) return null;
610
+ const re = /(\d+)\s*(h|m|s)/gi;
611
+ let total = 0, matched = false, m;
612
+ while ((m = re.exec(s)) !== null) {
613
+ matched = true;
614
+ const n = parseInt(m[1], 10);
615
+ const u = m[2].toLowerCase();
616
+ if (u === "h") total += n * 3600_000;
617
+ else if (u === "m") total += n * 60_000;
618
+ else total += n * 1000;
619
+ }
620
+ if (!matched || total < 5000) return null; // tối thiểu 5s — tránh hammer
621
+ return total;
622
+ }
623
+ function fmtMs(ms) {
624
+ if (ms < 60_000) return Math.round(ms / 1000) + "s";
625
+ if (ms < 3600_000) {
626
+ const m = Math.floor(ms / 60_000), s = Math.round((ms % 60_000) / 1000);
627
+ return s ? `${m}m${s}s` : `${m}m`;
628
+ }
629
+ const h = Math.floor(ms / 3600_000), mm = Math.round((ms % 3600_000) / 60_000);
630
+ return mm ? `${h}h${mm}m` : `${h}h`;
631
+ }
632
+ function stopLoop() {
633
+ if (!state.loop) return false;
634
+ clearInterval(state.loop.timer);
635
+ state.loop = null;
636
+ return true;
637
+ }
638
+ // Factory tạo tick handler — tách ra để cả runLoop và restore() dùng chung.
639
+ // Capture task qua tham số (không phải closure scope) → re-arm được sau resume.
640
+ function makeLoopTick(task) {
641
+ return async () => {
642
+ if (!state.loop) return; // đã bị stop
643
+ if (state.loop.running) return; // tick trước còn chạy → skip lượt này
644
+ state.loop.running = true;
645
+ state.loop.ticks++;
646
+ state.loop.lastTickAt = Date.now();
647
+ try {
648
+ console.log(c.dim(" " + t.loopTick(state.loop.ticks)));
649
+ const answer = await handle(loopTickPrompt(task, state.loop.ticks));
650
+ persist();
651
+ if (loopIsDone(answer)) {
652
+ console.log(c.ok(" ✓ " + t.loopAutoStop(state.loop.ticks)));
653
+ stopLoop();
654
+ }
655
+ // [GỠ BUDGET 2026-06-06] Không còn cap token cho /loop — loop dừng theo
656
+ // <<LOOP_DONE>> hoặc /loop stop, không bị cắt giữa chừng vì "hết token".
657
+ } catch (e) {
658
+ console.log(c.err(" loop tick lỗi: " + (e?.message || e)));
659
+ } finally {
660
+ if (state.loop) state.loop.running = false;
661
+ }
662
+ };
663
+ }
664
+ // /loop auto-stop: model phát token <<LOOP_DONE>> ở CUỐI reply khi thấy không
665
+ // còn việc / goal đã đạt → loop tự dừng. Combo tự nhiên với /goal: gõ /goal
666
+ // trước rồi /loop, model tự đánh giá goal mỗi tick và phát LOOP_DONE khi đủ.
667
+ const LOOP_DONE = "<<LOOP_DONE>>";
668
+ const loopIsDone = (a) => a && a.trimEnd().endsWith(LOOP_DONE);
669
+ const loopTickPrompt = (task, n) => `[LOOP tick #${n}] ${task}
670
+
671
+ Đâ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.
672
+ Sau khi xong tick này, TỰ ĐÁNH GIÁ:
673
+ - 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.
674
+ - 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.`;
675
+
676
+ async function runLoop(arg) {
677
+ const a = (arg || "").trim();
678
+ // /loop (no arg) → status
679
+ if (!a) {
680
+ if (!state.loop) return console.log(c.dim(" " + t.loopNotRunning) + c.dim(" " + t.loopNeedArgs));
681
+ const L = state.loop;
682
+ const elapsed = Date.now() - L.lastTickAt;
683
+ const nextIn = Math.max(0, L.intervalMs - elapsed);
684
+ return console.log(c.accent(" " + t.loopStatus(L.intervalStr, L.task, L.ticks, fmtMs(nextIn))));
685
+ }
686
+ // /loop stop
687
+ if (/^(stop|off|dừng|dung|tắt|tat)$/i.test(a)) {
688
+ if (stopLoop()) console.log(c.ok(" " + t.loopStopped));
689
+ else console.log(c.dim(" " + t.loopNotRunning));
690
+ return;
691
+ }
692
+ if (state.loop) return console.log(c.err(" " + t.loopAlreadyRunning));
693
+ if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
694
+ // parse <interval> <task>
695
+ // [GỠ BUDGET 2026-06-06] Cú pháp đơn giản: /loop <interval> <task>. Không còn cap token.
696
+ const firstSpace = a.search(/\s/);
697
+ if (firstSpace < 0) return console.log(c.err(" " + t.loopNeedArgs));
698
+ const intervalStr = a.slice(0, firstSpace).trim();
699
+ const task = a.slice(firstSpace + 1).trim();
700
+ if (!task) return console.log(c.err(" " + t.loopNeedArgs));
701
+ const intervalMs = parseInterval(intervalStr);
702
+ if (!intervalMs) return console.log(c.err(" " + t.loopBadInterval(intervalStr)));
703
+ const normInterval = fmtMs(intervalMs);
704
+ state.loop = {
705
+ intervalMs,
706
+ intervalStr: normInterval,
707
+ task,
708
+ ticks: 0,
709
+ startedAt: Date.now(),
710
+ lastTickAt: Date.now(),
711
+ running: false, // chống re-entrant (tick trước chưa xong, tick sau tới)
712
+ timer: null,
713
+ };
714
+ console.log(c.accent(" " + t.loopStarted(normInterval, task)));
715
+ state.loop.timer = setInterval(makeLoopTick(task), intervalMs);
716
+ // KHÔNG tick ngay — user có thể muốn gõ thêm lệnh khác trước khi tick đầu chạy.
717
+ }
718
+
461
719
  // /init — quét dự án & sinh noob.md tổng quan (giống `/init` của Claude Code).
462
720
  // Nếu noob.md đã có: hỏi xác nhận ghi đè trước khi giao việc cho model.
463
721
  async function runInit() {
@@ -748,18 +1006,28 @@ NGUYÊN TẮC:
748
1006
  console.log(chalk.hex("#8b5cf6")(` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`));
749
1007
  startSpin(t.thinking);
750
1008
  try {
751
- const results = await Promise.all(tasks.map((task, i) =>
752
- runSubAgent({
1009
+ const results = await Promise.all(tasks.map((task, i) => {
1010
+ // Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
1011
+ // findModel() resolve cả hai; nếu không match thì fallback model của cha.
1012
+ let subModel = state.model.id;
1013
+ let modelTag = "";
1014
+ if (task?.model) {
1015
+ const m = findModel(task.model);
1016
+ if (m) { subModel = m.id; modelTag = ` [${m.name}]`; }
1017
+ else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
1018
+ }
1019
+ // [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
1020
+ return runSubAgent({
753
1021
  task: task?.task || task?.prompt || "",
754
1022
  context: task?.context || "",
755
1023
  depth: depth + 1,
756
- model: state.model.id,
1024
+ model: subModel,
757
1025
  signal: abort.signal,
758
1026
  tokenMeter,
759
1027
  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
- ));
1028
+ onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg + modelTag)); startSpin(t.thinking); },
1029
+ }).then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`).catch((e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`);
1030
+ }));
763
1031
  return { allow: true, result: results.join("\n\n") };
764
1032
  } catch (err) {
765
1033
  return { allow: true, result: "ERROR sub-agent: " + (err?.message || String(err)) };
@@ -776,6 +1044,7 @@ NGUYÊN TẮC:
776
1044
  model: state.model.id,
777
1045
  signal: abort.signal,
778
1046
  tokenMeter,
1047
+ goal: state.goal,
779
1048
  extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
780
1049
  onStatus: () => tick(t.thinking),
781
1050
  onSteer: () => {
@@ -884,7 +1153,7 @@ NGUYÊN TẮC:
884
1153
 
885
1154
  tui.status(c.dim(" " + t.running));
886
1155
  try {
887
- const result = await runTool(name, input);
1156
+ const result = await runTool(name, input, { signal: abort?.signal });
888
1157
  tui.status(null);
889
1158
  console.log(c.ok(" ✓ ") + c.dim(firstLine(result)));
890
1159
  return { allow: true, result };
@@ -964,6 +1233,24 @@ NGUYÊN TẮC:
964
1233
  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
1234
  break;
966
1235
  }
1236
+ case "goal": {
1237
+ // HARD GOAL = completion requirement (xem tweet_dump.txt mục "Combine
1238
+ // with /goal and /loop"). Set xong sẽ inject vào MỌI prompt tới khi clear.
1239
+ const v = arg.trim();
1240
+ if (!v) {
1241
+ if (state.goal) console.log(c.accent(" 🎯 goal: ") + state.goal);
1242
+ else console.log(c.dim(" chưa đặt goal. Cú pháp: /goal <mục tiêu> · /goal clear để xoá"));
1243
+ } else if (v.toLowerCase() === "clear" || v.toLowerCase() === "off" || v.toLowerCase() === "xoá" || v.toLowerCase() === "xoa") {
1244
+ state.goal = null;
1245
+ console.log(c.dim(" đã xoá goal"));
1246
+ persist();
1247
+ } else {
1248
+ state.goal = v;
1249
+ console.log(c.accent(" 🎯 đã đặt goal: ") + v);
1250
+ persist();
1251
+ }
1252
+ break;
1253
+ }
967
1254
  case "tokens": {
968
1255
  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
1256
  break;
@@ -982,6 +1269,11 @@ NGUYÊN TẮC:
982
1269
  case "fd":
983
1270
  await runFrontendDesign(arg);
984
1271
  break;
1272
+ case "workflow":
1273
+ case "wf":
1274
+ case "ultracode":
1275
+ await runWorkflow(arg);
1276
+ break;
985
1277
  case "improve":
986
1278
  case "imp":
987
1279
  await runImprove(arg);
@@ -990,6 +1282,9 @@ NGUYÊN TẮC:
990
1282
  case "u":
991
1283
  await runUltra(arg);
992
1284
  break;
1285
+ case "loop":
1286
+ await runLoop(arg);
1287
+ break;
993
1288
  case "init":
994
1289
  await runInit();
995
1290
  break;
@@ -1264,6 +1559,9 @@ function printHelp() {
1264
1559
  " " + t.cmdFrontendDesign,
1265
1560
  " " + t.cmdImprove,
1266
1561
  " " + t.cmdUltra,
1562
+ " " + t.cmdWorkflow,
1563
+ " " + t.cmdGoal,
1564
+ " " + t.cmdLoop,
1267
1565
  " " + t.cmdLearn,
1268
1566
  " " + t.cmdCompact,
1269
1567
  " " + t.cmdMemory,
package/src/subagent.js CHANGED
@@ -1,67 +1,126 @@
1
- // Sub-agent: chạy một runAgent() con với history độc lập, dùng chung tool
2
- // runtime của cha. Hỗ trợ phân cấp (sub-agent thể đẻ sub-agent tiếp) nhưng
3
- // giới hạn độ sâu để không nổ. Hỗ trợ song song qua spawn_agents (mảng).
4
- import { runAgent } from "./agent.js";
5
- import { TokenMeter } from "./tokens.js";
1
+ // Sub-agent: chạy một runAgent() con với history riêng, dùng chung dispatcher tool
2
+ // của cha. Cha (repl.js) truyền `dispatchTool` vào cha source of truth cho:
3
+ // - permission/approve UI
4
+ // - in log tool-call
5
+ // - xử spawn_agent lồng nhau (cha tự tăng depth khi forward xuống)
6
+ //
7
+ // Contract với cha (xem repl.js dispatcher spawn_agent/spawn_agents):
8
+ // Cha gọi: runSubAgent({ task, context, model, signal, tokenMeter, dispatchTool, depth, onLog })
9
+ // - dispatchTool(name, input) → Promise<{ allow, result }> (cùng format runAgent.onTool kỳ vọng)
10
+ // - tokenMeter: cha truyền meter của phiên → token sub-agent cộng dồn vào tổng
11
+ // - signal: cha truyền abort.signal → cha Ctrl+C thì con dừng theo
12
+ // - depth: cha tăng sẵn (depth+1) trước khi gọi; sub-agent chỉ dùng để biết còn được spawn cháu hay không
13
+ //
14
+ // Trả về: Promise<string> — text cuối của sub-agent, để cha ghép vào tool result.
15
+ //
16
+ // Depth guard: MAX_SUBAGENT_DEPTH = 3. Khi depth >= MAX, sub-agent vẫn chạy nhưng
17
+ // extraToolsDoc rỗng → model không thấy spawn_agent nữa, không spawn cháu được.
18
+ // Cha cũng có guard riêng (line ~1001 repl.js) chặn spawn vượt MAX, đây là defence-in-depth.
19
+
20
+ import { runAgent } from './agent.js';
6
21
 
7
22
  export const MAX_SUBAGENT_DEPTH = 3;
8
23
 
9
- // Tài liệu tool spawn_agent — chèn vào prompt khi agent mode bật. Mô tả cho
10
- // model biết WHEN dùng (task lớn, chia được) và HOW (song song vs tuần tự).
11
- export function spawnAgentToolsDoc(depth = 0) {
12
- const canSpawn = depth < MAX_SUBAGENT_DEPTH;
13
- return `# AGENT MODE — multi-agent
24
+ export async function runSubAgent({
25
+ task,
26
+ context = '',
27
+ model,
28
+ signal,
29
+ tokenMeter,
30
+ dispatchTool,
31
+ depth = 1,
32
+ onLog,
33
+ }) {
34
+ if (typeof dispatchTool !== 'function') {
35
+ throw new Error('runSubAgent: dispatchTool (từ cha) là bắt buộc');
36
+ }
37
+ if (!task || typeof task !== 'string') {
38
+ throw new Error('runSubAgent: task (string) là bắt buộc');
39
+ }
14
40
 
15
- Bạn đang chế độ AGENT. Khi gặp task LỚNthể chia nhỏ, hãy ủy thác cho sub-agent thay tự làm hết một mình:
41
+ // Prompt khởi tạo: nêu task + context (nếu). Sub-agent history riêng,
42
+ // KHÔNG kế thừa từ cha → giảm nhiễu, tiết kiệm token.
43
+ const userPrompt = context
44
+ ? `# Nhiệm vụ con\n${task}\n\n# Ngữ cảnh từ agent cha\n${context}`
45
+ : `# Nhiệm vụ con\n${task}`;
16
46
 
17
- - spawn_agent {"task": str, "context"?: str} — đẻ MỘT sub-agent làm "task". "context" là phần ngữ cảnh cần truyền (file paths, quyết định đã chốt, ràng buộc). Sub-agent có TOÀN BỘ tool (read/write/edit/run/grep/glob…) và history RIÊNG (không thấy hội thoại của bạn). Nó trả về một chuỗi tóm tắt kết quả.
18
- - spawn_agents {"tasks": [{"task": str, "context"?: str}, …]} — đẻ NHIỀU sub-agent CHẠY SONG SONG. Chỉ dùng khi các task ĐỘC LẬP (không phụ thuộc kết quả của nhau). Trả về mảng tóm tắt theo đúng thứ tự tasks.
47
+ const history = [{ role: 'user', content: userPrompt }];
19
48
 
20
- Quy tắc dùng:
21
- 1. TUẦN TỰ (task B cần kết quả task A): gọi spawn_agent cho A, đọc kết quả, rồi spawn_agent cho B. KHÔNG dùng spawn_agents.
22
- 2. SONG SONG (các task không liên quan): dùng MỘT lần spawn_agents với mảng tasks. Tiết kiệm thời gian.
23
- 3. PHÂN CẤP (task phức tạp): sub-agent của bạn cũng có spawn_agent, nó tự chia tiếp. Độ sâu tối đa hiện tại: ${MAX_SUBAGENT_DEPTH} (bạn đang ở depth=${depth}${canSpawn ? "" : " — đã chạm trần, KHÔNG được spawn nữa, tự làm"}).
24
- 4. Việc NHỎ/đơn giản: cứ tự làm, đừng spawn cho có. Spawn có overhead (mỗi sub-agent là 1 phiên model riêng → tốn token).
25
- 5. Sau khi gom kết quả từ sub-agent, BẠN là người tổng hợp + trả lời cuối cho user. Sub-agent không nói chuyện trực tiếp với user.
49
+ // Nếu chưa chạm trần depth, cho phép spawn cháu. Chạm trần → bỏ doc.
50
+ const extraToolsDoc =
51
+ depth < MAX_SUBAGENT_DEPTH ? spawnAgentToolsDoc(depth) : '';
26
52
 
27
- dụ song song: "viết test cho 3 module độc lập" spawn_agents với 3 tasks.
28
- dụ tuần tự: "thiết kế schema rồi viết migration" → spawn_agent(thiết kế) đọc → spawn_agent(viết migration với schema đó).
29
- dụ phân cấp: cha giao "build full app" → đẻ 1 sub-agent "build backend" → sub-agent đó tự đẻ tiếp "viết auth", "viết CRUD" song song.`;
30
- }
53
+ // onTool: forward thẳng dispatcher cha. Cha sẽ tự tăng depth khi spawn lồng
54
+ // (xem repl.js line 1027: `(n, inp) => dispatchTool(n, inp, depth + 1)`).
55
+ const onTool = async (name, input) => dispatchTool(name, input);
56
+
57
+ if (onLog) onLog(`▶ sub-agent depth=${depth} bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
31
58
 
32
- // Chạy một sub-agent. dispatchTool: hàm để thực thi tool con (chia sẻ với cha).
33
- // model: dùng chung model của cha. onLog: callback để log tiến độ ra UI cha.
34
- export async function runSubAgent({ task, context, model, signal, dispatchTool, depth = 1, onLog, tokenMeter }) {
35
- const sys = `Bạn là SUB-AGENT (depth=${depth}) được agent cha ủy thác MỘT nhiệm vụ cụ thể.
36
-
37
- # Cách làm việc
38
- - Tự quyết với thông tin được cấp + tự khám phá filesystem (list_dir/glob/grep/read_file). KHÔNG hỏi lại cha.
39
- - History của bạn TÁCH BIỆT với cha. Cha CHỈ thấy chuỗi trả lời cuối của bạn → hãy là một bản tóm tắt cô đọng (mục tiêu 1–2k token): mọi file đã đụng, phát hiện then chốt, lỗi/cảnh báo, và các đầu mối cha cần để hành động tiếp. Bỏ chi tiết quá trình thừa.
40
- - Làm điều nhỏ nhất giải quyết trọn vẹn nhiệm vụ. Không drive-by refactor.
41
- - Verify khi hợp lý (chạy build/test/lint). Báo trung thực phần đã/chưa verify.
42
-
43
- # NHIỆM VỤ
44
- ${task}
45
- ${context ? `\n# NGỮ CẢNH TỪ CHA\n${context}` : ""}`;
46
- const history = [{ role: "user", content: sys }];
47
- // Dùng chung meter của cha nếu được truyền vào → token sub-agent cộng dồn
48
- // vào tổng phiên. Nếu không có thì tự tạo cục bộ (giữ tương thích cũ).
49
- const meter = tokenMeter || new TokenMeter();
50
- const before = { input: meter.input, output: meter.output };
51
- onLog?.(`↳ sub-agent (depth=${depth}) bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? "…" : ""}`);
52
59
  const result = await runAgent({
53
60
  history,
54
61
  model,
55
62
  signal,
56
- tokenMeter: meter,
57
- extraToolsDoc: spawnAgentToolsDoc(depth),
58
- onTool: (name, input) => dispatchTool(name, input, depth),
59
- onStatus: () => {},
60
- onDelta: () => {},
61
- onSteer: () => [],
63
+ tokenMeter,
64
+ onTool,
65
+ extraToolsDoc,
66
+ goal: task,
62
67
  });
63
- const used = { input: meter.input - before.input, output: meter.output - before.output };
64
- onLog?.(`↳ sub-agent (depth=${depth}) xong (↑${used.input} ↓${used.output})`);
65
- // Trả về string sạch để cha (model) đọc dễ. Token đã cộng vào meter rồi.
66
- return result;
68
+
69
+ if (onLog) onLog(`✓ sub-agent depth=${depth} xong`);
70
+
71
+ // runAgent trả về text cuối (assistant message không có tool block).
72
+ return typeof result === 'string' ? result : String(result ?? '');
73
+ }
74
+
75
+ // Tài liệu tool spawn_agent / spawn_agents để chèn vào prompt khi agent mode bật.
76
+ // Dài & cụ thể vì model cần đủ context để dùng đúng (routing, depth, format).
77
+ export function spawnAgentToolsDoc(depth = 0) {
78
+ const remaining = MAX_SUBAGENT_DEPTH - depth;
79
+ if (remaining <= 0) return '';
80
+ return `
81
+ # Sub-agent tools (agent mode đang BẬT, depth=${depth}, MAX_SUBAGENT_DEPTH=${MAX_SUBAGENT_DEPTH})
82
+
83
+ Khi nhiệm vụ phức tạp / chia được thành nhiều phần độc lập, bạn có thể spawn sub-agent để xử lý song song hoặc cô lập ngữ cảnh. Sub-agent có history riêng (không kế thừa từ cha → giảm nhiễu + tiết kiệm token), dùng chung token meter + signal abort với cha.
84
+
85
+ ## Tools
86
+ - spawn_agent {"task": str, "context"?: str, "model"?: str}
87
+ Chạy 1 sub-agent độc lập, trả về kết quả dạng string (text cuối của sub-agent).
88
+ - spawn_agents {"agents": [{"task": str, "context"?: str, "model"?: str}, ...]}
89
+ Chạy NHIỀU sub-agent SONG SONG (Promise.all). Trả về mảng kết quả ghép theo thứ tự, mỗi phần có header "── sub-agent #N ──".
90
+
91
+ ## Rules
92
+ 1. TASK PHẢI CỤ THỂ: nêu rõ goal + output mong đợi (vd "đọc src/api.js, liệt kê tất cả endpoint + method, trả về dạng bảng markdown"). Đừng giao task mơ hồ kiểu "phân tích code".
93
+ 2. CONTEXT TRUYỀN GỌN: chỉ trích đoạn cần thiết (path, snippet, dữ liệu chốt). Không dump cả history cha vào — sub-agent có history riêng, dump = lãng phí token.
94
+ 3. KHÔNG GIỚI HẠN TOKEN — sub-agent chạy tới khi xong task hoặc gặp lỗi/abort. KHÔNG set field token_budget (đã gỡ khỏi runtime).
95
+ 4. DEPTH GUARD: depth hiện tại = ${depth}. Depth còn lại: ${remaining}. Khi depth >= ${MAX_SUBAGENT_DEPTH}, sub-agent KHÔNG thấy spawn_agent nữa (chống nổ đệ quy).
96
+ 5. ROUTING MODEL (field "model"?: str — TỐI ƯU CHI PHÍ):
97
+ - Task đơn giản (đọc file, tóm tắt, grep, format): dùng model rẻ — vd "deepseek-v4-flash", "gpt-5-mini", "kimi".
98
+ - Task khó (refactor đa file, debug bug phức tạp, review kỹ thuật): dùng model mạnh — vd "claude-opus-4-7", "gpt-5", "deepseek-v4".
99
+ - Bỏ field "model" hoặc để rỗng → sub-agent kế thừa model của cha (mặc định an toàn).
100
+ - Tên model: gõ ngắn gọn ("claude-opus-4-7", "kimi", "o3-mini") — runtime tự fuzzy match (bỏ prefix gateway-, chuẩn hoá dấu/space).
101
+ 6. KHI NÀO DÙNG spawn_agents (song song) vs spawn_agent (đơn):
102
+ - Song song khi N task độc lập, không phụ thuộc nhau (vd review 5 file riêng biệt, fan-out + synthesize, generate-and-filter).
103
+ - Đơn lẻ khi chỉ cô lập 1 task nặng (giữ history cha sạch) hoặc cần kết quả của task này trước khi quyết spawn task sau.
104
+ 7. SAU KHI NHẬN KẾT QUẢ: tổng hợp/synthesize trong agent cha — đừng paste nguyên block kết quả ra cho user. Sub-agent là worker, cha là orchestrator.
105
+
106
+ ## Ví dụ
107
+ spawn_agent đơn:
108
+ \`\`\`tool
109
+ {"name": "spawn_agent", "input": {"task": "Đọc src/api.js, liệt kê tất cả hàm export kèm signature ngắn (tên + tham số). Trả về list markdown.", "context": "File ESM, dùng fetch streaming.", "model": "deepseek-v4-flash"}}
110
+ \`\`\`
111
+
112
+ spawn_agents song song (fan-out review 3 file):
113
+ \`\`\`tool
114
+ {"name": "spawn_agents", "input": {"agents": [
115
+ {"task": "Review src/agent.js — tìm bug logic, race condition. Trả về list bullet.", "model": "claude-opus-4-7"},
116
+ {"task": "Review src/api.js — kiểm error handling streaming. Trả về list bullet.", "model": "claude-opus-4-7"},
117
+ {"task": "Review src/tools.js — kiểm input validation. Trả về list bullet.", "model": "claude-opus-4-7"}
118
+ ]}}
119
+ \`\`\`
120
+
121
+ ## Anti-pattern
122
+ - KHÔNG spawn sub-agent cho task 1 bước (vd "đọc 1 file rồi trả về") — tự đọc bằng read_file rẻ hơn nhiều.
123
+ - KHÔNG spawn nested sâu vô tội vạ — mỗi cấp depth = 1 lần gọi model + 1 history riêng = đắt.
124
+ - KHÔNG để sub-agent tự spawn tiếp khi task của nó đã đơn giản — depth guard chỉ là cứu cánh, không phải license đệ quy.
125
+ `;
67
126
  }
package/src/tokens.js CHANGED
@@ -100,4 +100,20 @@ export class TokenMeter {
100
100
  this._tail = "";
101
101
  this._tailTokens = 0;
102
102
  }
103
+ // Serialize counter để persist qua --continue/--resume. Tail buffer là transient
104
+ // (chỉ phục vụ tính output realtime trong 1 lượt), KHÔNG cần lưu — endOutput()
105
+ // đã commit hết vào this.output trước khi persist() chạy ở cuối mỗi lượt.
106
+ serialize() {
107
+ return { input: this.input, output: this.output };
108
+ }
109
+ // Khôi phục từ snapshot — chỉ set 2 counter, tail giữ rỗng (lượt mới bắt đầu).
110
+ restore(data) {
111
+ if (!data || typeof data !== "object") return;
112
+ this.input = Math.max(0, data.input | 0);
113
+ this.output = Math.max(0, data.output | 0);
114
+ this._committedChars = 0;
115
+ this._committedTokens = 0;
116
+ this._tail = "";
117
+ this._tailTokens = 0;
118
+ }
103
119
  }