@hmharness/agent 0.4.4 → 0.5.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/dist/prompt.d.ts CHANGED
@@ -6,4 +6,5 @@ export declare function buildSystemPrompt(opts: {
6
6
  insights: string;
7
7
  model: string;
8
8
  locale?: string;
9
+ agentsMd?: string;
9
10
  }): string;
package/dist/prompt.js CHANGED
@@ -4,9 +4,32 @@ export function buildSystemPrompt(opts) {
4
4
  const isWin = process.platform === 'win32';
5
5
  parts.push(`You are hmh, a coding agent powered by ${opts.model}, running on hmharness - a self-evolving agent framework designed for the full HarmonyOS development lifecycle. Working directory: ${opts.cwd}.`, '', opts.locale === 'en'
6
6
  ? 'Reply in the language the user writes in (English by default).'
7
- : '回复语言跟随用户(默认使用中文)。', '', '## Host environment (facts - rely on these, do not guess)', `- OS: ${process.platform} (${process.arch}). The run_command tool executes through ${isWin ? 'cmd.exe (Windows cmd - NOT bash, NOT PowerShell)' : '/bin/sh'}.`, isWin
7
+ : '回复语言跟随用户(默认使用中文)。',
8
+ // ---- [Codex] 持久执行 ----
9
+ '', '## Execution mandate', 'Persist until the task is fully handled end-to-end within the current turn: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes. Only stop when the user explicitly pauses, redirects, or when the turn budget is genuinely exhausted. For multi-step tasks: (1) outline your approach in 2-4 steps before acting; (2) after completing each step, confirm it succeeded before moving to the next; (3) if verification fails, diagnose and retry - do not skip forward. Skip planning only for trivial one-liners.',
10
+ // ---- [Codex] 并行优先 ----
11
+ '', 'When multiple independent tool calls are needed (e.g. fetching several files, checking multiple paths, running unrelated diagnostics), invoke them in the same turn rather than one by one. The framework executes independent calls concurrently - this is the single biggest speed-up available to you.',
12
+ // ---- [Codex + DeepSeek] 代码编辑纪律 ----
13
+ '', '## Code discipline', 'Prefer `edit_file` for surgical changes (search-replace of a specific string). Only use `write_file` for new files or complete rewrites. Never overwrite a file you have not read. You may be in a dirty git worktree: NEVER revert existing changes you did not make unless the user asks. If you notice unexpected changes in files you have not touched, STOP and ask the user. Never amend a commit unless explicitly requested. When asked for a "review", default to code review mindset: prioritize bugs, risks, regressions, missing tests - findings first, ordered by severity with file:line references.',
14
+ // ---- [DeepSeek] 分段执行 + 合理质疑 ----
15
+ '', 'For tasks requiring reasoning across multiple steps, work in phases: search and list the key facts, plan the concrete steps, then execute. Do not skip planning to act directly.', 'If the user\'s requested approach carries risk or there is a clearly better alternative, raise it proactively - state the tradeoffs and let the user choose. Only execute confirmed plans.',
16
+ // ---- [Codex] 用户反馈 ----
17
+ '', "When the user gives feedback saying something didn't work, treat it as ground truth. Use available tools to verify the current state, identify the root cause, fix it, and explain what changed.",
18
+ // ---- 宿主环境 ----
19
+ '', '## Host environment (facts - rely on these, do not guess)', `- OS: ${process.platform} (${process.arch}). The run_command tool executes through ${isWin ? 'cmd.exe (Windows cmd - NOT bash, NOT PowerShell)' : '/bin/sh'}.`, isWin
8
20
  ? '- cmd.exe has NO grep/head/tail/ls/cat/$(...) and wmic may be absent. Use: findstr (search), "more" or node -e (head/text ops), dir /b (ls), type (cat), where (which). For anything richer: powershell -NoProfile -Command "..."'
9
- : '- standard POSIX utilities are available.', `- npm workspaces monorepo; agent state lives in HMH_HOME (${opts.home}): config.json, memory, skills, insights, sessions.`, '- Investigate before asking the user: read environment variables (any *API_KEY), check listening ports (netstat -ano | findstr LISTENING) and probe http://127.0.0.1:<port>/v1/models to discover local services. Never scan a whole drive (dir /s /b from a root) - it times out; search specific directories instead.', '', '## Where tools and skills live on this machine (check these FIRST when asked "is X installed")', `Other agent frameworks share this machine; their skills/plugins/tools are at known paths under the home directory. When asked to check whether a tool/skill/package is installed, or to find/configure an existing one, probe these locations (in order) before concluding "not installed":`, `- ~/.zcode/skills/<name>/ (ZCode skills with SKILL.md, scripts/, .env)`, `- ~/.claude/skills/<name>/ (Claude Code skills with SKILL.md, scripts/)`, `- ~/.codex/skills/<name>/ (Codex skills with SKILL.md, scripts/)`, `- ~/.dsh/skills/<name>/ and ~/.dsh-hm/skills/<name>/ (deepseek-harness skills)`, `- ~/.jcode/skills/<name>/ (JCode skills)`, `- ~/.agent-reach-venv/ and ~/.agent-reach/tools/ (Agent Reach CLI + platform tools)`, `- ~/.local/bin/ (user-installed executables; PATH may not include it - check dir /b not just where)`, `Home directory: ${homedir()}`, 'When found, read the tool\'s SKILL.md or README.md for its usage; copy or reference its scripts rather than reinventing. If the user asks to "configure it for hmharness", check whether it exposes an MCP endpoint (add to config.json mcpServers), an HTTP API (add a provider), or a CLI (register as a skill via `hmh skills add <path>`).', '', 'HarmonyOS development is your home domain: DevEco Studio toolchain, hvigor builds, ohpm packages, hdc devices, ArkTS/ArkUI, OpenHarmony and Cangjie. When a task touches it, prefer the harmony_* tools and precise toolchain knowledge.', '', 'Working style: read before writing; prefer small focused commands; verify results; state tradeoffs briefly. For risky operations (deleting, overwriting, publishing) say what will happen first. When a command fails twice with the same error, switch strategy instead of repeating it. For multi-line/quoted logic, write a temp .cjs file and run it with node - never fight cmd.exe quoting with node -e one-liners.');
21
+ : '- standard POSIX utilities are available.', `- npm workspaces monorepo; agent state lives in HMH_HOME (${opts.home}): config.json, memory, skills, insights, sessions.`, '- Investigate before asking the user: read environment variables (any *API_KEY), check listening ports (netstat -ano | findstr LISTENING) and probe http://127.0.0.1:<port>/v1/models to discover local services. Never scan a whole drive (dir /s /b from a root) - it times out; search specific directories instead.',
22
+ // ---- 跨框架技能路径 ----
23
+ '', '## Where tools and skills live on this machine (check these FIRST when asked "is X installed")', `Other agent frameworks share this machine; their skills/plugins/tools are at known paths under the home directory. When asked to check whether a tool/skill/package is installed, or to find/configure an existing one, probe these locations (in order) before concluding "not installed":`, `- ~/.zcode/skills/<name>/ (ZCode skills with SKILL.md, scripts/, .env)`, `- ~/.claude/skills/<name>/ (Claude Code skills with SKILL.md, scripts/)`, `- ~/.codex/skills/<name>/ (Codex skills with SKILL.md, scripts/)`, `- ~/.dsh/skills/<name>/ and ~/.dsh-hm/skills/<name>/ (deepseek-harness skills)`, `- ~/.jcode/skills/<name>/ (JCode skills)`, `- ~/.agent-reach-venv/ and ~/.agent-reach/tools/ (Agent Reach CLI + platform tools)`, `- ~/.local/bin/ (user-installed executables; PATH may not include it - check dir /b not just where)`, `Home directory: ${homedir()}`, 'When found, read the tool\'s SKILL.md or README.md for its usage; copy or reference its scripts rather than reinventing. If the user asks to "configure it for hmharness", check whether it exposes an MCP endpoint (add to config.json mcpServers), an HTTP API (add a provider), or a CLI (register as a skill via `hmh skills add <path>`).',
24
+ // ---- 鸿蒙域 ----
25
+ '', 'HarmonyOS development is your home domain: DevEco Studio toolchain, hvigor builds, ohpm packages, hdc devices, ArkTS/ArkUI, OpenHarmony and Cangjie. When a task touches it, prefer the harmony_* tools and precise toolchain knowledge.',
26
+ // ---- 通用工作风格 ----
27
+ '', 'Working style: read before writing; prefer small focused commands; verify results; state tradeoffs briefly. For risky operations (deleting, overwriting, publishing) say what will happen first. When a command fails twice with the same error, switch strategy instead of repeating it. For multi-line/quoted logic, write a temp .cjs file and run it with node - never fight cmd.exe quoting with node -e one-liners.');
28
+ // ---- AGENTS.md / CLAUDE.md / .cursorrules 注入(被动发现,不需模型主动调用) ----
29
+ if (opts.agentsMd?.trim()) {
30
+ parts.push('', '## Project-level instructions (AGENTS.md / CLAUDE.md / .cursorrules)', opts.agentsMd.trim());
31
+ }
32
+ // ---- 进化上下文注入 ----
10
33
  if (opts.memory.trim()) {
11
34
  parts.push('', '## Long-term memory', opts.memory.trim());
12
35
  }
package/dist/runner.d.ts CHANGED
@@ -43,7 +43,19 @@ export declare function contextPack(task: string, sessionId?: string, opts?: {
43
43
  * Terminal approval gate: auto mode passes everything; a TTY gets a y/N
44
44
  * prompt (reusing a caller-provided readline); a pipe gets a safe deny.
45
45
  * The kernel loop denies by default when no gate is wired at all.
46
+ *
47
+ * Persistent approval (Codex's .rules pattern): once a user approves a
48
+ * command pattern (e.g. "hdc shell"), it is saved to
49
+ * HMH_HOME/approved-rules.json and auto-approved next time. Rules are
50
+ * matched by the tool name + args prefix. The hard-deny patterns in
51
+ * tools.ts always override rules - dangerous commands are never auto-approved.
46
52
  */
53
+ export interface ApprovedRule {
54
+ tool: string;
55
+ argPrefix: string;
56
+ time: string;
57
+ }
58
+ export declare function loadApprovedRules(home: string): ApprovedRule[];
47
59
  export declare function makeApproval(cfg: HmhConfig, yes: boolean, sharedRl?: readline.Interface): LoopApproval;
48
60
  export interface RunnerEvents {
49
61
  onLine?(line: string): void;
package/dist/runner.js CHANGED
@@ -6,6 +6,9 @@
6
6
  * approval gate construction.
7
7
  */
8
8
  import { homeDir, loadConfig, resolveProvider, mcpServerTools, Registry, runLoop, Session, } from '@hmharness/kernel';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { readFileSync, writeFileSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
9
12
  import { appendMemory, listSkills, readInsights, readNotes, recentInsights, recordInsight, retrieveMemory, skillsToPrompt, sessionGetsCanary, canaryWatermark, listCanary, workspaceForCwd } from '@hmharness/evolution';
10
13
  import { harmonyTools } from '@hmharness/domain-harmony';
11
14
  import { opsTools } from '@hmharness/domain-ops';
@@ -72,6 +75,26 @@ export async function buildRegistry(opts = {}) {
72
75
  }
73
76
  return { reg, clients };
74
77
  }
78
+ /** Discover AGENTS.md / CLAUDE.md / .cursorrules by walking up from cwd to
79
+ * the workspace root. Deeper files take precedence (Codex convention). Only
80
+ * the first hit is returned; null when nothing found. */
81
+ async function discoverAgentsMd(cwd) {
82
+ const NAMES = ['AGENTS.md', 'CLAUDE.md', '.cursorrules'];
83
+ let dir = cwd;
84
+ while (true) {
85
+ for (const name of NAMES) {
86
+ try {
87
+ return await readFile(join(dir, name), 'utf8');
88
+ }
89
+ catch { /* not here */ }
90
+ }
91
+ const parent = dirname(dir);
92
+ if (parent === dir)
93
+ break; // filesystem root
94
+ dir = parent;
95
+ }
96
+ return null;
97
+ }
75
98
  /** Retrieval-based context pack: task-relevant memories, not the whole file.
76
99
  * P0 canary: ~20% of sessions (deterministic by session id) also receive
77
100
  * the canary skill block, watermarked as experimental references - the
@@ -94,17 +117,34 @@ export async function contextPack(task, sessionId, opts = {}) {
94
117
  }
95
118
  return { memory, skills: skillsToPrompt(skills) + (canaryBlock ? '\n' + canaryBlock : ''), insights, skillsInjected: [...skills.map((s) => s.name), ...canaryNames] };
96
119
  }
97
- /**
98
- * Terminal approval gate: auto mode passes everything; a TTY gets a y/N
99
- * prompt (reusing a caller-provided readline); a pipe gets a safe deny.
100
- * The kernel loop denies by default when no gate is wired at all.
101
- */
120
+ export function loadApprovedRules(home) {
121
+ try {
122
+ return JSON.parse(readFileSync(join(home, 'approved-rules.json'), 'utf8'));
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ }
128
+ function saveApprovedRules(home, rules) {
129
+ try {
130
+ writeFileSync(join(home, 'approved-rules.json'), JSON.stringify(rules, null, 2));
131
+ }
132
+ catch { /* best effort */ }
133
+ }
134
+ function matchesRule(rules, toolName, args) {
135
+ const argsStr = JSON.stringify(args);
136
+ return rules.some((r) => r.tool === toolName && argsStr.startsWith(r.argPrefix));
137
+ }
102
138
  export function makeApproval(cfg, yes, sharedRl) {
103
139
  const t = strings(cfg.locale ?? 'zh');
140
+ const home = homeDir();
104
141
  return {
105
142
  async ask(toolName, args) {
106
143
  if (yes || cfg.approval === 'auto')
107
144
  return true;
145
+ // Persistent rules: patterns the user previously approved are auto-passed
146
+ if (matchesRule(loadApprovedRules(home), toolName, args))
147
+ return true;
108
148
  const brief = JSON.stringify(args).slice(0, 120);
109
149
  if (!stdin.isTTY) {
110
150
  process.stdout.write(`\x1b[33m${t.approvalDeniedNoTty(toolName, brief)}\x1b[0m\n`);
@@ -119,7 +159,17 @@ export function makeApproval(cfg, yes, sharedRl) {
119
159
  if (!sharedRl)
120
160
  rl.close();
121
161
  }
122
- return answer === 'y' || answer === 'yes';
162
+ const granted = answer === 'y' || answer === 'yes';
163
+ // Save approved patterns for future auto-approval (skip simple argless tools)
164
+ if (granted && Object.keys(args).length > 0) {
165
+ const rules = loadApprovedRules(home);
166
+ const argsStr = JSON.stringify(args);
167
+ if (!rules.some((r) => r.tool === toolName && r.argPrefix === argsStr)) {
168
+ rules.push({ tool: toolName, argPrefix: argsStr, time: new Date().toISOString() });
169
+ saveApprovedRules(home, rules);
170
+ }
171
+ }
172
+ return granted;
123
173
  },
124
174
  };
125
175
  }
@@ -140,6 +190,7 @@ export async function runAgentTask(opts) {
140
190
  // contextPack needs the session id: canary injection is deterministic
141
191
  // per-session (stable attribution), decided before the prompt is built
142
192
  const pack = await contextPack(opts.task, session.id, { workspace, embedding });
193
+ const agentsMd = await discoverAgentsMd(ctx.cwd);
143
194
  const system = buildSystemPrompt({
144
195
  cwd: ctx.cwd,
145
196
  home: ctx.home,
@@ -148,6 +199,7 @@ export async function runAgentTask(opts) {
148
199
  insights: pack.insights,
149
200
  model: cfg.provider.model,
150
201
  locale: cfg.locale,
202
+ agentsMd: agentsMd ?? undefined,
151
203
  });
152
204
  await session.user(opts.task);
153
205
  const approval = opts.approvalAsk ? { ask: opts.approvalAsk } : makeApproval(cfg, opts.yes === true);
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { type Tool } from '@hmharness/kernel';
2
2
  export declare const readFileTool: Tool;
3
3
  export declare const writeFileTool: Tool;
4
+ /** Surgical search-replace tool (adopted from Codex's apply_patch philosophy).
5
+ * Safer than write_file for targeted edits: only changes the declared fragment,
6
+ * refuses to run when old_string is not unique (prevents silent wrong-location
7
+ * edits), and does NOT need approval - the blast radius is bounded to the
8
+ * declared substring. The model must read the file first to know what to replace. */
9
+ export declare const editFileTool: Tool;
4
10
  export declare const listDirTool: Tool;
5
11
  /** Host-shell pipe preflight: on Windows cmd, Unix-isms fail with cryptic
6
12
  * mojibake and the model retries for many turns. Only the FIRST word of
package/dist/tools.js CHANGED
@@ -73,6 +73,48 @@ export const writeFileTool = {
73
73
  }
74
74
  },
75
75
  };
76
+ /** Surgical search-replace tool (adopted from Codex's apply_patch philosophy).
77
+ * Safer than write_file for targeted edits: only changes the declared fragment,
78
+ * refuses to run when old_string is not unique (prevents silent wrong-location
79
+ * edits), and does NOT need approval - the blast radius is bounded to the
80
+ * declared substring. The model must read the file first to know what to replace. */
81
+ export const editFileTool = {
82
+ name: 'edit_file',
83
+ description: 'Surgical search-replace edit. Provide old_string (must appear exactly once in the file) and new_string to replace it. Prefer this over write_file for changes to existing files. old_string must be unique - if it appears more than once, the edit is refused (make it more specific).',
84
+ parameters: {
85
+ type: 'object',
86
+ properties: {
87
+ path: { type: 'string', description: 'file path' },
88
+ old_string: { type: 'string', description: 'the exact string to find and replace (must appear exactly once in the file)' },
89
+ new_string: { type: 'string', description: 'the replacement string' },
90
+ },
91
+ required: ['path', 'old_string', 'new_string'],
92
+ },
93
+ async execute(args, ctx) {
94
+ try {
95
+ const p = safePath(String(args.path), ctx.cwd);
96
+ const old = String(args.old_string ?? '');
97
+ const rep = String(args.new_string ?? '');
98
+ if (!old)
99
+ return { output: 'old_string is empty - provide the text to replace', isError: true };
100
+ if (old === rep)
101
+ return { output: 'old_string and new_string are identical - nothing to change', isError: true };
102
+ const content = await readFile(p, 'utf8');
103
+ const idx = content.indexOf(old);
104
+ if (idx < 0)
105
+ return { output: `old_string not found in ${p} - read the file first to find the exact text`, isError: true };
106
+ const second = content.indexOf(old, idx + 1);
107
+ if (second >= 0)
108
+ return { output: `old_string is not unique in ${p} (found at offset ${idx} and ${second}) - make it more specific by including surrounding context`, isError: true };
109
+ const out = content.slice(0, idx) + rep + content.slice(idx + old.length);
110
+ await writeFile(p, out, 'utf8');
111
+ return { output: `edited ${p}: replaced ${old.length} chars with ${rep.length} chars at offset ${idx}` };
112
+ }
113
+ catch (err) {
114
+ return { output: String(err), isError: true };
115
+ }
116
+ },
117
+ };
76
118
  export const listDirTool = {
77
119
  name: 'list_dir',
78
120
  description: 'List a directory: names with d/- prefix and size.',
@@ -589,4 +631,4 @@ export const seeImageTool = {
589
631
  return { output: `all vision providers failed:\n${errors.join('\n')}`, isError: true };
590
632
  },
591
633
  };
592
- export const baseTools = [readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool];
634
+ export const baseTools = [readFileTool, editFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/agent",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,10 +15,10 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/kernel": "0.4.4",
19
- "@hmharness/evolution": "0.4.4",
20
- "@hmharness/domain-harmony": "0.4.4",
21
- "@hmharness/domain-ops": "0.4.4"
18
+ "@hmharness/kernel": "0.5.0",
19
+ "@hmharness/evolution": "0.5.0",
20
+ "@hmharness/domain-harmony": "0.5.0",
21
+ "@hmharness/domain-ops": "0.5.0"
22
22
  },
23
23
  "files": [
24
24
  "dist"