mocode-ai 1.2.6 → 1.2.7

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.
@@ -0,0 +1,102 @@
1
+ // skill 使用台账(自进化 Phase 0)。
2
+ // 为什么不在 trace.jsonl 上做:工具事件出于隐私只存参数指纹(sha256/keys,
3
+ // trace-sanitize.ts 刻意不存值),拿不到 skill name。台账因此在工具层直接记录——
4
+ // use_skill / run_skill 是唯一知道真实 skill name 的落点。
5
+ //
6
+ // 落盘:<cwd>/.mocode/skill-stats.jsonl(append-only JSONL,每行一次使用)。
7
+ // 纯观测:任何写失败静默吞掉,绝不阻断 agent 主流程(风格对齐 session/trace.ts)。
8
+ // 聚合是纯函数(aggregateSkillStats),吃记录数组吐按 skill 的计数,便于单测。
9
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ /**
12
+ * 台账路径(项目级,与 sessions 同根;使用是项目上下文相关的,不写全局)。
13
+ * baseDir 可选:测试指向临时目录用;缺省 process.cwd()。
14
+ */
15
+ export function skillStatsPath(baseDir = process.cwd()) {
16
+ return path.join(baseDir, '.mocode', 'skill-stats.jsonl');
17
+ }
18
+ /**
19
+ * 追加一条台账记录;任何失败静默(观测不得阻断主流程)。
20
+ * MOCODE_SKILL_EVAL=1(触发评测进程内设置)时跳过:评测里的人工构造调用
21
+ * 是测量手段不是真实使用,记入会污染自进化的输入信号。
22
+ */
23
+ export function recordSkillUsage(rec, baseDir = process.cwd()) {
24
+ if (process.env.MOCODE_SKILL_EVAL === '1')
25
+ return;
26
+ try {
27
+ const p = skillStatsPath(baseDir);
28
+ const dir = path.dirname(p);
29
+ if (!existsSync(dir))
30
+ mkdirSync(dir, { recursive: true });
31
+ appendFileSync(p, JSON.stringify(rec) + '\n', 'utf8');
32
+ }
33
+ catch {
34
+ // 观测失败静默
35
+ }
36
+ }
37
+ /** 读取台账原始记录;文件不存在 / 单行损坏 → 跳过(不抛)。 */
38
+ export function loadSkillUsage(baseDir = process.cwd()) {
39
+ try {
40
+ const content = readFileSync(skillStatsPath(baseDir), 'utf8');
41
+ const out = [];
42
+ for (const line of content.split('\n')) {
43
+ const s = line.trim();
44
+ if (!s)
45
+ continue;
46
+ try {
47
+ const v = JSON.parse(s);
48
+ if (v && typeof v.skill === 'string' && (v.kind === 'use' || v.kind === 'run')) {
49
+ out.push(v);
50
+ }
51
+ }
52
+ catch {
53
+ // 损坏行跳过
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ /**
63
+ * 纯聚合:记录 → 按 skill 的计数视图。按 skill 名分组(大小写敏感),
64
+ * lastUsedAt 取 ts 字符串字典序最大(ISO 时间戳字典序 == 时间序)。
65
+ * 输入乱序也安全。空输入返回 []。
66
+ */
67
+ export function aggregateSkillStats(records) {
68
+ const bySkill = new Map();
69
+ for (const r of records) {
70
+ const list = bySkill.get(r.skill);
71
+ if (list)
72
+ list.push(r);
73
+ else
74
+ bySkill.set(r.skill, [r]);
75
+ }
76
+ const out = [];
77
+ for (const [skill, list] of bySkill) {
78
+ const runs = list.filter((r) => r.kind === 'run');
79
+ const runSuccess = runs.filter((r) => r.status === 'success').length;
80
+ let lastFailure = null;
81
+ let lastUsedAt = '';
82
+ for (const r of list) {
83
+ if (typeof r.ts === 'string' && r.ts > lastUsedAt)
84
+ lastUsedAt = r.ts;
85
+ if (r.status !== 'success' && (!lastFailure || r.ts > lastFailure.ts)) {
86
+ lastFailure = { ts: r.ts, status: r.status, code: r.code };
87
+ }
88
+ }
89
+ out.push({
90
+ skill,
91
+ total: list.length,
92
+ uses: list.length - runs.length,
93
+ runs: runs.length,
94
+ runSuccessRate: runs.length ? runSuccess / runs.length : null,
95
+ lastFailure,
96
+ lastUsedAt,
97
+ });
98
+ }
99
+ // 按最近使用倒序,让 /skills 徽标与人工浏览都「最活跃的在前」。
100
+ out.sort((a, b) => (a.lastUsedAt < b.lastUsedAt ? 1 : a.lastUsedAt > b.lastUsedAt ? -1 : 0));
101
+ return out;
102
+ }
@@ -220,7 +220,11 @@ export async function executeToolOutcome(name, argsRaw, signal, opts) {
220
220
  }
221
221
  const validation = validateToolArguments(tool, parsed);
222
222
  if (!validation.valid) {
223
- return terminalOutcome('error', validation.code, `错误:工具 ${name} 参数无效: ${validation.message}`, startedAt);
223
+ const hint = opts?.argumentErrorHint?.trim();
224
+ const message = hint
225
+ ? `错误:工具 ${name} 参数无效: ${validation.message}\n${hint}`
226
+ : `错误:工具 ${name} 参数无效: ${validation.message}`;
227
+ return terminalOutcome('error', validation.code, message, startedAt);
224
228
  }
225
229
  const args = parsed;
226
230
  const sandboxError = enforceSandbox(name, args);
package/dist/ui/layout.js CHANGED
@@ -671,6 +671,17 @@ export function contentDeleteFrom(startIdx, n) {
671
671
  export function totalRows() {
672
672
  return content.totalRows();
673
673
  }
674
+ /** 缓冲尾部(已提交行)是否已经是空白行(去掉 ANSI 后无可见字符)。
675
+ * 供 compact 等在 step 循环顶部写通知行前判断是否需要补空行分隔。 */
676
+ export function isLastContentRowBlank() {
677
+ const committed = content.committedRows();
678
+ if (committed === 0)
679
+ return false;
680
+ const line = content.lineAt(committed - 1);
681
+ if (line === null)
682
+ return false;
683
+ return line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
684
+ }
674
685
  /** 正文→mutation 首摘要前,把尾部间距强制归一为一条视觉空行。 */
675
686
  export function normalizeMutationBoundary() {
676
687
  if (!active || !ui.isTTY)
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { discoverPackageValidationCommands } from './discovery.js';
3
+ import { discoverProjectProfile } from './profile.js';
4
+ /** Keep the prompt section small on large monorepos; the agent can still discover the rest. */
5
+ const MAX_LISTED_PACKAGES = 8;
6
+ function displayRoot(profile, packageProfile) {
7
+ const relative = path.relative(profile.root, packageProfile.root);
8
+ return relative === '' ? '.' : relative.split(path.sep).join('/');
9
+ }
10
+ function lineFor(profile, packageProfile) {
11
+ const commands = discoverPackageValidationCommands(profile, packageProfile);
12
+ if (commands.length === 0)
13
+ return null;
14
+ const cwd = displayRoot(profile, packageProfile);
15
+ const rendered = commands.map((item) => `\`${item.command}\``).join(', ');
16
+ return `- ${packageProfile.name} (cwd \`${cwd}\`): ${rendered}`;
17
+ }
18
+ /**
19
+ * Deterministic project validation map injected into the system prompt: which package owns which
20
+ * script, and the exact command plus cwd to run it. Commands are listed in increasing cost order
21
+ * (typecheck → build → test) and are never executed here — this is evidence, not a completion gate.
22
+ *
23
+ * Returns '' when no package exposes a validation script, or when discovery fails for any reason
24
+ * (missing/invalid manifest, unreadable workspace): prompt construction must never break.
25
+ */
26
+ export function buildValidationCommandsSection(root = process.cwd()) {
27
+ try {
28
+ const profile = discoverProjectProfile(root);
29
+ const lines = [];
30
+ let omitted = 0;
31
+ for (const packageProfile of profile.packages) {
32
+ const line = lineFor(profile, packageProfile);
33
+ if (!line)
34
+ continue;
35
+ if (lines.length >= MAX_LISTED_PACKAGES)
36
+ omitted += 1;
37
+ else
38
+ lines.push(line);
39
+ }
40
+ if (lines.length === 0)
41
+ return '';
42
+ if (omitted > 0) {
43
+ lines.push(`- …${omitted} more package(s) with scripts: read their package.json when needed.`);
44
+ }
45
+ return [
46
+ '',
47
+ '## Validation commands (discovered from project manifests)',
48
+ 'Listed in increasing cost order. Use them when a check is worth running; prefer the package that owns your change over repository-wide runs. Not a completion gate.',
49
+ ...lines,
50
+ ].join('\n');
51
+ }
52
+ catch {
53
+ return ''; // Discovery is best-effort: never let it break prompt construction.
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "build": "tsc -p tsconfig.build.json",
24
24
  "test": "tsc -p tsconfig.test-build.json && node --test --experimental-test-isolation=none \"dist-tests/tests/*.test.js\"",
25
25
  "typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
26
- "eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts",
26
+ "eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/work-discipline.ts",
27
27
  "eval:coding": "tsx evals/coding/runner.ts",
28
28
  "eval:coding:list": "tsx evals/coding/runner.ts --list",
29
29
  "prepare": "npm run build"