s-app-scoder-agent 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -272,7 +272,7 @@ The autonomous loop is wrapped by a configurable rule engine (`src/guardrails.ts
272
272
  | `chat-done` | Ends conversational turns (marker-based) without a build |
273
273
  | `completion-claim` | Never trusts "the task is complete" — forces an independent build verification |
274
274
  | `text-only-streak` | Ends the run after 2 consecutive tool-less steps with no file changes (the "chào" failure mode) |
275
- | `continue-nudge` | Nudges plain-text answers back to work |
275
+ | `continue-nudge` | Nudges plain-text answers back to work (no build demand when no files changed — suggests the `<<CHAT_DONE>>` exit instead) |
276
276
  | `doom-loop` | Intervenes after 3× identical consecutive tool-call batches |
277
277
  | `build-stop` | Ends the run when the exact build command succeeds; re-verifies on failure |
278
278
  | `history-trim` | Char-budget history trimming with whole-turn drops + old-tool-result pruning |
@@ -371,9 +371,10 @@ scoder tui "fix the bug" # auto-submit an initial task
371
371
  Features:
372
372
 
373
373
  - **Alternate screen buffer** (vim/htop style) — the app owns the full screen: the header stays pinned at the top, input + status bar at the bottom, the chat pane scrolls internally, and your terminal content is restored on exit. Terminals without alt-screen support (`TERM=dumb`, non-TTY) automatically render inline instead.
374
- - **Build Rail** (pinned top bar) — brand, build lamp (`● build ok` / `● build fail` / `no build yet`), permission mode, and a step fuel gauge (`▰▰▰▱▱▱▱▱▱▱`) that fills as the agent burns through its step budget- **Phase breadcrumb + live metrics** (second header row, while running) — `plan › explore › edit › build › verify` (current phase highlighted) on the left; tokens in→out, **tok/s**, elapsed time, and guardrail count on the right
375
- - **Chat pane** — your messages, streaming assistant text with a live block cursor (`▋`), and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running, wall-clock duration per tool), prefixed with a flight-recorder step number (`03▸`)
376
- - **Header row 2** — phase breadcrumb (`plan › explore › edit › build › verify`), live metrics (tokens in→out, **tok/s**, elapsed, guardrail count), and a human-readable one-line notice of the latest guardrail intervention (e.g. `⚠ agent reminder: agent trả lời text mà không dùng tool — nhắc tiếp tục làm việc`)
374
+ - **Build Rail** (pinned top bar, single row) — brand, build lamp (`✓ ok` / `✗ fail` / `no build yet`), permission mode, and a center region that shows either the phase breadcrumb (`plan › explore › edit › build › verify`) or the latest guardrail notice spanning the full available width (e.g. `⚠ trả lời text chưa dùng tool — nhắc tiếp tục`)
375
+ - **Sidebar (≥92 cols)** — SESSION (model + workdir, no permission line), METRICS (step fuel gauge, tokens, tok/s, ctx input/limit, cost, elapsed, guardrail count), PHASES, BUILD, MCP, PLAN (live checklist at the bottom; resets on each new user message)
376
+ - **Chat pane** — your messages with a full colored border + `you ▸` label, streaming assistant text with a live block cursor (`▋`), and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running, wall-clock duration per tool), prefixed with a flight-recorder step number (`03▸`)
377
+ - **Header row 2** — (no longer present; phase breadcrumb + metrics moved into the sidebar / status bar to avoid duplication)
377
378
  - **Diff cards** (`✎`) — every file write/edit renders as a unified diff (`+` green / `-` red / `@@` cyan hunk headers); the same diff also flows out of the HTTP SSE stream
378
379
  - **Status bar** — model, token usage (input/output/cached), scroll position, keybind hints
379
380
  - **Slash commands** — `/help`, `/clear`, `/cost`, `/model`, `/skills`, `/skill`, `/mcp`, `/review`, `/permission`, `/exit` (identical to the REPL)
package/dist/agent.js CHANGED
@@ -176,6 +176,10 @@ export class Agent {
176
176
  catch (err) {
177
177
  return { ok: false, output: `ERROR: subagent failed: ${err.message}` };
178
178
  }
179
+ finally {
180
+ // Child agents are ephemeral: close their MCP connections (if any).
181
+ child.dispose();
182
+ }
179
183
  };
180
184
  this.ctx.taskRunner = runner;
181
185
  }
@@ -626,7 +630,12 @@ export class Agent {
626
630
  this.tools = [...allToolSpecs(), ...mcpSpecs];
627
631
  }
628
632
  }
629
- /** Disconnect MCP servers at the end of a run. */
633
+ /**
634
+ * Disconnect MCP servers when the agent is disposed — connections are kept
635
+ * alive for the whole agent/session lifetime (one "connected" line, no
636
+ * re-spawn per message). Ephemeral owners (server runs, one-shot CLI) call
637
+ * dispose() when their agent is done.
638
+ */
630
639
  shutdownMcp() {
631
640
  this.mcp?.killAll();
632
641
  this.mcp = null;
@@ -689,13 +698,8 @@ export class Agent {
689
698
  buildCmd,
690
699
  permission: this.config.permissionMode,
691
700
  });
692
- try {
693
- await this.connectMcp();
694
- return await this.runLoop(0, buildCmd, null);
695
- }
696
- finally {
697
- this.shutdownMcp();
698
- }
701
+ await this.connectMcp();
702
+ return this.runLoop(0, buildCmd, null);
699
703
  }
700
704
  /**
701
705
  * One conversational turn in an ongoing session (used by the REPL):
@@ -742,13 +746,8 @@ export class Agent {
742
746
  this.messages.push({ role: "user", content });
743
747
  this.pendingContext = [];
744
748
  this.messages.push({ role: "user", content: task });
745
- try {
746
- await this.connectMcp();
747
- return await this.runLoop(0, buildCmd, null);
748
- }
749
- finally {
750
- this.shutdownMcp();
751
- }
749
+ await this.connectMcp();
750
+ return this.runLoop(0, buildCmd, null);
752
751
  }
753
752
  /**
754
753
  * Resume a previous session: restore messages/step count and continue the loop.
@@ -782,13 +781,8 @@ export class Agent {
782
781
  : null;
783
782
  // v2: opening a resumed run stamps a fresh run.started (event log continuity).
784
783
  this.events.append({ type: "run.started", task: this.currentTask });
785
- try {
786
- await this.connectMcp();
787
- return await this.runLoop(session.steps ?? 0, buildCmd, lastBuild);
788
- }
789
- finally {
790
- this.shutdownMcp();
791
- }
784
+ await this.connectMcp();
785
+ return this.runLoop(session.steps ?? 0, buildCmd, lastBuild);
792
786
  }
793
787
  /**
794
788
  * The shared autonomous loop:
@@ -167,6 +167,20 @@ const continueNudgeRule = { name: "continue-nudge",
167
167
  return null;
168
168
  if (claimsCompletion(ctx.text))
169
169
  return null; // completion-claim rule phụ trách
170
+ // No file changed yet → never demand a build for what may be a pure Q&A or
171
+ // an explanation-only answer. Give the model the explicit conversational
172
+ // exit instead; text-only-streak still ends the run if it keeps stalling.
173
+ if (!ctx.filesModified) {
174
+ return {
175
+ action: {
176
+ kind: "inject",
177
+ reminder: `Continue working on the task. Use tools only if code changes are needed. ` +
178
+ `If the user only asked a question and no code change is needed, do NOT run the build — ` +
179
+ `end your reply with ${CHAT_DONE_MARKER} on its own last line instead.`,
180
+ },
181
+ reason: "plain text answer without tool calls (no files modified)",
182
+ };
183
+ }
170
184
  return {
171
185
  action: {
172
186
  kind: "inject",
package/dist/index.js CHANGED
@@ -220,6 +220,9 @@ async function runOnce(cfg, task) {
220
220
  ui.error(err.message);
221
221
  return 2;
222
222
  }
223
+ finally {
224
+ agent.dispose();
225
+ }
223
226
  }
224
227
  /**
225
228
  * `scoder init` — first-run wizard: asks for the essentials, writes
@@ -323,11 +326,13 @@ async function repl(cfg) {
323
326
  config: cfg,
324
327
  replaceAgent: (next) => {
325
328
  agent.abort();
329
+ agent.dispose();
326
330
  agent = next ?? new Agent(cfg);
327
331
  first = true;
328
332
  },
329
333
  resumeAndRun: async (s) => {
330
334
  agent.abort();
335
+ agent.dispose();
331
336
  agent = new Agent(cfg);
332
337
  first = false;
333
338
  await agent.resume(s);
@@ -357,6 +362,7 @@ async function repl(cfg) {
357
362
  ui.error(err.message);
358
363
  }
359
364
  }
365
+ agent.dispose();
360
366
  ui.info("bye");
361
367
  process.exit(0);
362
368
  }
@@ -419,9 +425,11 @@ async function main() {
419
425
  });
420
426
  try {
421
427
  const result = await agent.resume(s, args.task ?? undefined);
428
+ agent.dispose();
422
429
  process.exit(result.success ? 0 : 1);
423
430
  }
424
431
  catch (err) {
432
+ agent.dispose();
425
433
  ui.error(err.message);
426
434
  process.exit(2);
427
435
  }
package/dist/prompt.js CHANGED
@@ -3,18 +3,14 @@
3
3
  * - versioning rõ ràng (PROMPT_VERSION được ghi vào session JSONL khi chạy),
4
4
  * - test được độc lập (không cần instantiate Agent).
5
5
  *
6
- * V3 — stricter scope rules, clarification-before-action, safety boundaries,
7
- * MCP/skills as passive listings only:
8
- * - ORIENT: read relevant files before ANY edit
9
- * - PLAN: specific and actionable, not generic
10
- * - EDIT: match existing style, minimal diffs
11
- * - VERIFY: read the FULL error, fix the cause not the symptom
12
- * - Safety & Boundaries: explicit prohibitions on git hooks, CI configs,
13
- * dependencies, infrastructure configs, and destructive commands
14
- * - Tool selection: concrete descriptions with explicit usage rules
15
- * - Skills/MCP: passive listing, never auto-load
6
+ * V4 — conversational exits + no wasted builds:
7
+ * - restore the <<CHAT_DONE>> marker rule (V3 dropped it, so the chat-done
8
+ * guardrail could never fire and every Q&A turn was nudged into a build)
9
+ * - build only after file edits; never run the build when nothing changed
10
+ * - V3 baseline: stricter scope rules, clarification-before-action, safety
11
+ * boundaries, MCP/skills as passive listings only
16
12
  */
17
- export const PROMPT_VERSION = 3;
13
+ export const PROMPT_VERSION = 4;
18
14
  export const SYSTEM_PROMPT = `You are Scoder, an autonomous coding agent running in a terminal.
19
15
  You complete coding tasks end-to-end: exploring the code, editing files, and
20
16
  verifying your work with the build command. You act; you don't just advise.
@@ -30,9 +26,9 @@ verifying your work with the build command. You act; you don't just advise.
30
26
  3. EDIT: Make small, focused changes. Match the file's existing style
31
27
  (indentation, quote style, naming conventions) — do NOT reformat unrelated
32
28
  parts. Never refactor unrelated code, never add speculative features.
33
- 4. VERIFY: Read the FULL error output before editing — fix the CAUSE, not the
34
- symptom. Errors can be misleading (e.g. 'X not found' might be caused by a
35
- missing import upstream). Then run the build command.
29
+ 4. VERIFY: If you changed files, run the build command. Read the FULL error
30
+ output before editing — fix the CAUSE, not the symptom. Errors can be
31
+ misleading (e.g. 'X not found' might be caused by a missing import upstream).
36
32
  5. REPORT: Your final message states what changed, why, and anything left
37
33
  unresolved. Be honest: if something didn't work, say exactly what failed.
38
34
 
@@ -46,7 +42,8 @@ verifying your work with the build command. You act; you don't just advise.
46
42
  - **write_file**: Create or overwrite. Parent dirs created automatically. Use for
47
43
  new files or when write is cleaner than targeted edits.
48
44
  - **run_command**: Run shell command. Captures stdout + stderr. THIS IS THE BUILD
49
- COMMAND — every task ends with run_command(<buildCmd>).
45
+ COMMAND — run it only when you changed files; a coding task ends with
46
+ run_command(<buildCmd>).
50
47
  - **write_todos**: Plan + live progress for multi-step work.
51
48
  - **web_fetch**: Exact URLs only (it cannot search). Use for library/API docs,
52
49
  versions, changelogs when the answer isn't in the working directory.
@@ -74,11 +71,20 @@ verifying your work with the build command. You act; you don't just advise.
74
71
  - Improve code quality outside the task scope
75
72
  2. If the request is ambiguous or the scope is unclear: ask ONE clarifying question
76
73
  before taking action. Do NOT guess and proceed — clarify first.
77
- 3. When you believe the task is done, call run_command with the build command.
78
- The loop ends only when the build succeeds.
79
- 4. If run_command is denied by permission policy, find a safer alternative or
74
+ 3. When you believe the task is done:
75
+ - If you changed files, call run_command with the build command. The coding
76
+ loop ends only when the build succeeds.
77
+ - If you changed nothing (you answered a question or explained code), do NOT
78
+ run the build command — a build with no changes proves nothing and wastes
79
+ time.
80
+ 4. CONVERSATIONAL INPUT: if the user's message is purely conversational — a
81
+ greeting, thanks, or a question needing no code changes and no shell
82
+ commands — do NOT use tools, do NOT edit files, and do NOT run a build.
83
+ Reply briefly in the user's language and end your reply with the exact marker
84
+ <<CHAT_DONE>> on its own last line.
85
+ 5. If run_command is denied by permission policy, find a safer alternative or
80
86
  report the limitation clearly. Do not retry the same denied command.
81
- 5. If an environmental problem makes the task impossible, stop and report it.
87
+ 6. If an environmental problem makes the task impossible, stop and report it.
82
88
 
83
89
  ## Quality bar
84
90
 
package/dist/server.js CHANGED
@@ -83,7 +83,10 @@ export function startServer(baseConfig, opts = {}) {
83
83
  record.finishedAt = new Date().toISOString();
84
84
  for (const sub of record.subscribers)
85
85
  sub();
86
- });
86
+ })
87
+ // MCP connections live for the agent lifetime — each server run owns an
88
+ // ephemeral agent, so dispose it here (never leak child processes).
89
+ .finally(() => agent.dispose());
87
90
  };
88
91
  const handleEventsSSE = (req, res, record) => {
89
92
  res.writeHead(200, {
package/dist/tui/app.js CHANGED
@@ -61,6 +61,12 @@ export function TuiApp({ config, initialTask }) {
61
61
  const historyRef = useRef([]);
62
62
  const runningRef = useRef(false);
63
63
  const agentRef = useRef(null);
64
+ // MCP connections live for the session (one connect, no per-message re-spawn).
65
+ // Dispose on unmount so child server processes never outlive the TUI.
66
+ useEffect(() => () => {
67
+ agentRef.current?.dispose();
68
+ agentRef.current = null;
69
+ }, []);
64
70
  // ── Prompt-gate review flow (/review) ──────────────────────────────────────
65
71
  // State machine relaying the universal-prompt-gate-mcp text-fallback flow:
66
72
  // call → pending_level_selection → user picks level → pending_user_selection
@@ -214,6 +220,7 @@ export function TuiApp({ config, initialTask }) {
214
220
  isRunning: () => runningRef.current,
215
221
  replaceAgent: (next) => {
216
222
  agentRef.current?.abort();
223
+ agentRef.current?.dispose();
217
224
  agentRef.current = next;
218
225
  if (!next) {
219
226
  runningRef.current = false;
@@ -114,8 +114,9 @@ function TodoItemView() {
114
114
  export function ChatItemView({ item }) {
115
115
  switch (item.kind) {
116
116
  case "user":
117
- // Clean user bubble: accent arrow + bold text
118
- return (_jsxs(Box, { marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { color: theme.user, bold: true, children: "\u276F " }), _jsx(Text, { bold: true, children: item.text })] }));
117
+ // User messages stand out with a full colored border + bold label so they're
118
+ // immediately visible at a glance, distinct from the agent's ◆ markdown.
119
+ return (_jsx(Box, { marginTop: 1, marginLeft: 1, children: _jsxs(Box, { borderStyle: "single", borderColor: theme.user, paddingX: 1, paddingTop: 0, flexDirection: "column", children: [_jsx(Text, { color: theme.user, bold: true, children: "you \u25B8" }), _jsx(Text, { bold: true, children: item.text })] }) }));
119
120
  case "assistant": {
120
121
  // Streaming: dim with pulsing cursor; finalized: markdown with agent mark.
121
122
  if (item.streaming || !item.text) {
@@ -23,7 +23,7 @@ export function gaugeColor(step, maxSteps) {
23
23
  /**
24
24
  * BuildRail — polished top bar (SINGLE row).
25
25
  *
26
- * Layout: [⚑ scoder] ── [phase breadcrumb / guardrail warning] ── [perm] [build] [steps]
26
+ * Layout: [⚑ scoder] ── [phase breadcrumb / guardrail warning] ── [perm] [build]
27
27
  *
28
28
  * Terminal ≥ SIDEBAR_MIN_COLS: sidebar handles METRICS + PHASES.
29
29
  * Terminal < SIDEBAR_MIN_COLS: phase breadcrumb + metrics move into this row.
@@ -37,15 +37,15 @@ export function BuildRail({ state, config }) {
37
37
  const lampColor = state.buildLamp === "ok" ? theme.success : state.buildLamp === "fail" ? theme.error : theme.dim;
38
38
  const lampLabel = state.buildLamp === "ok" ? "✓" : state.buildLamp === "fail" ? "✗" : "○";
39
39
  const lampText = state.buildLamp === "ok" ? "ok" : state.buildLamp === "fail" ? "fail" : "—";
40
- // Step badge
41
- const stepLabel = state.step > 0
42
- ? `${state.step}${state.maxSteps > 0 ? `/${state.maxSteps}` : ""}`
43
- : "idle";
44
- // Guardrail: shown inline if it fired (small warning before the phase breadcrumb)
40
+ // Guardrail: shown inline if it fired (small warning before the phase breadcrumb).
45
41
  const hasGuardrail = Boolean(state.lastGuardrail);
42
+ // Guardrail text gets the whole center region — no fixed 40-char cap that
43
+ // truncates while the header still has empty space. Reserve room for the
44
+ // brand on the left and permission/build lamps on the right.
45
+ const centerBudget = Math.max(24, (columns ?? 100) - 34);
46
46
  // Phase breadcrumb or narrow-terminal metrics (right of the center)
47
47
  const showNarrowMetrics = !wide && (state.status === "running" || state.phases.length > 0);
48
- return (_jsxs(Box, { borderStyle: "bold", borderColor: theme.border, paddingX: 1, justifyContent: "space-between", alignItems: "center", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: theme.brand, children: ["\u2691 ", _jsx(Text, { bold: true, children: "scoder" })] }), _jsx(Box, { flexGrow: 1, justifyContent: "center", children: hasGuardrail ? (_jsxs(Text, { color: theme.warn, bold: true, children: ["\u26A0 ", fit(state.lastGuardrail, 40)] })) : state.status === "running" ? (showNarrowMetrics ? (_jsx(PhaseBreadcrumb, { phases: state.phases })) : (_jsx(Text, { dimColor: true, children: "plan \u203A explore \u203A edit \u203A build \u203A verify" }))) : (_jsx(Text, { dimColor: true, children: "ready" })) }), _jsxs(Text, { children: [_jsxs(Text, { color: permColor, children: ["[", perm, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: lampColor, children: lampLabel }), _jsxs(Text, { dimColor: true, children: [" ", lampText] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: stepLabel })] })] }));
48
+ return (_jsxs(Box, { borderStyle: "bold", borderColor: theme.border, paddingX: 1, justifyContent: "space-between", alignItems: "center", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: theme.brand, children: ["\u2691 ", _jsx(Text, { bold: true, children: "scoder" })] }), _jsx(Box, { flexGrow: 1, justifyContent: "center", children: hasGuardrail ? (_jsxs(Text, { color: theme.warn, bold: true, children: ["\u26A0 ", fit(state.lastGuardrail, centerBudget)] })) : state.status === "running" ? (showNarrowMetrics ? (_jsx(PhaseBreadcrumb, { phases: state.phases })) : (_jsx(Text, { dimColor: true, children: "plan \u203A explore \u203A edit \u203A build \u203A verify" }))) : (_jsx(Text, { dimColor: true, children: "ready" })) }), _jsxs(Text, { children: [_jsxs(Text, { color: permColor, children: ["[", perm, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: lampColor, children: lampLabel }), _jsxs(Text, { dimColor: true, children: [" ", lampText] })] })] }));
49
49
  }
50
50
  /** Breadcrumb tiến trình: plan › explore › edit › build › verify. */
51
51
  const PHASE_SEQUENCE = ["plan", "explore", "edit", "build", "verify"];
@@ -15,11 +15,9 @@ function SectionLine({ label, value, color }) {
15
15
  }
16
16
  // ── Session ──────────────────────────────────────────────────────────────────
17
17
  function SessionSection({ state, config }) {
18
- const perm = state.permission || config.permissionMode;
19
- const permColor = perm === "readonly" ? theme.warn : perm === "ask" ? theme.accent : theme.dim;
20
18
  const model = state.model || config.model || "—";
21
19
  const dir = config.workdir.split("/").filter(Boolean).pop() ?? config.workdir;
22
- return (_jsxs(Section, { title: "SESSION", children: [_jsx(SectionLine, { label: "model", value: fit(model, INNER - 9) }), _jsx(SectionLine, { label: "perm", value: perm, color: permColor }), _jsx(SectionLine, { label: "dir", value: fit(dir, INNER - 9) })] }));
20
+ return (_jsxs(Section, { title: "SESSION", children: [_jsx(SectionLine, { label: "model", value: fit(model, INNER - 9) }), _jsx(SectionLine, { label: "dir", value: fit(dir, INNER - 9) })] }));
23
21
  }
24
22
  function PlanRow({ t, width }) {
25
23
  if (t.status === "completed") {
@@ -97,5 +95,5 @@ function McpSection({ state, config }) {
97
95
  // ── Root ─────────────────────────────────────────────────────────────────────
98
96
  /** Sidebar phải — mọi trạng thái sống của run, cập nhật theo AgentEvent. */
99
97
  export function Sidebar({ state, config }) {
100
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.border, width: SIDEBAR_WIDTH, paddingX: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(SessionSection, { state: state, config: config }), _jsx(PlanSection, { state: state }), _jsx(MetricsSection, { state: state }), _jsx(PhaseSection, { state: state }), _jsx(BuildSection, { state: state, config: config }), _jsx(McpSection, { state: state, config: config })] }));
98
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.border, width: SIDEBAR_WIDTH, paddingX: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(SessionSection, { state: state, config: config }), _jsx(MetricsSection, { state: state }), _jsx(PhaseSection, { state: state }), _jsx(BuildSection, { state: state, config: config }), _jsx(McpSection, { state: state, config: config }), _jsx(PlanSection, { state: state })] }));
101
99
  }
package/dist/tui/state.js CHANGED
@@ -69,12 +69,24 @@ export function visibleWindow(items, viewportRows, margin = 50) {
69
69
  /**
70
70
  * Plan mới nhất cho sidebar — todo items được append mỗi lần update nhưng
71
71
  * sidebar chỉ cần snapshot hiện tại (last-wins), null khi chưa có plan.
72
+ *
73
+ * Reset realtime: mỗi lần user gửi message mới, plan reset — chỉ hiện plan
74
+ * được viết SAU user message cuối cùng. Tránh stale plan từ task trước hiện
75
+ * trong task mới.
72
76
  */
73
77
  export function latestPlan(items) {
78
+ // Tìm user message cuối cùng — plan chỉ reset tại đây.
79
+ let lastUserIdx = -1;
74
80
  for (let i = items.length - 1; i >= 0; i--) {
75
- const it = items[i];
76
- if (it.kind === "todo")
77
- return it;
81
+ if (items[i].kind === "user") {
82
+ lastUserIdx = i;
83
+ break;
84
+ }
85
+ }
86
+ // Chỉ tìm todo SAU user cuối.
87
+ for (let i = items.length - 1; i > lastUserIdx; i--) {
88
+ if (items[i].kind === "todo")
89
+ return items[i];
78
90
  }
79
91
  return null;
80
92
  }
@@ -120,13 +132,11 @@ function applyGuardrail(state, ev) {
120
132
  "chat-done": "lượt chat thuần — kết thúc không cần build",
121
133
  "completion-claim": "agent báo hoàn thành — buộc chạy build xác minh",
122
134
  "text-only-streak": "agent chỉ nói không làm — dừng run",
123
- "continue-nudge": "agent trả lời text mà không dùng tool — nhắc tiếp tục làm việc",
135
+ "continue-nudge": "trả lời text chưa dùng tool — nhắc tiếp tục",
124
136
  "doom-loop": "lặp lẫn lộn cùng tool call — yêu cầu đổi hướng",
125
137
  "build-stop": "build pass — kết thúc run",
126
138
  };
127
139
  state.lastGuardrail = HUMAN[ev.rule] ?? `${ev.rule}: ${ev.reason}`;
128
- if (state.lastGuardrail.length > 72)
129
- state.lastGuardrail = state.lastGuardrail.slice(0, 69) + "…";
130
140
  }
131
141
  /**
132
142
  * Fold one AgentEvent into the state (mutates `state`, returns it for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s-app-scoder-agent",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Autonomous coding agent CLI — writes, edits, and builds code autonomously until the project compiles without errors.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",