@hmharness/agent 0.23.21 → 0.23.23

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,23 @@
1
+ /**
2
+ * @hmharness/agent - Code World Model recorder (production sensor wiring)
3
+ *
4
+ * Round 38 closes the loop the sensors were waiting for: extension page
5
+ * observations now reach a PERSISTED Code World Model. The recorder is a
6
+ * lazy per-home singleton — the first observation loads the model from
7
+ * HMH_HOME, the sensor feeds it, and a debounce flushes to disk. The LSP
8
+ * sensor (round 35) rides the same store once its tools gain an event
9
+ * hook; until then it stays library-only, honestly.
10
+ *
11
+ * Failure posture: everything best-effort — a storage error logs nothing
12
+ * and never breaks the tool that observed the page (evidence must not be
13
+ * able to kill the work it evidences). A crash loses at most one debounce
14
+ * window of observations.
15
+ */
16
+ import { type CodeWorldModel } from '@hmharness/cognitive';
17
+ import type { RawPageData } from '@hmharness/extension';
18
+ /** The onPageRead hook handed to extensionTools — one closure per home. */
19
+ export declare function pageReadSink(home: string): (raw: RawPageData) => void;
20
+ /** Test isolation: drop caches + pending timers (never used in production). */
21
+ export declare function resetCodeWorldRecorder(): void;
22
+ /** Introspection for tests/status surfaces: the live model for a home. */
23
+ export declare function liveCodeWorldModel(home: string): Promise<CodeWorldModel | null>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @hmharness/agent - Code World Model recorder (production sensor wiring)
3
+ *
4
+ * Round 38 closes the loop the sensors were waiting for: extension page
5
+ * observations now reach a PERSISTED Code World Model. The recorder is a
6
+ * lazy per-home singleton — the first observation loads the model from
7
+ * HMH_HOME, the sensor feeds it, and a debounce flushes to disk. The LSP
8
+ * sensor (round 35) rides the same store once its tools gain an event
9
+ * hook; until then it stays library-only, honestly.
10
+ *
11
+ * Failure posture: everything best-effort — a storage error logs nothing
12
+ * and never breaks the tool that observed the page (evidence must not be
13
+ * able to kill the work it evidences). A crash loses at most one debounce
14
+ * window of observations.
15
+ */
16
+ import { loadCodeWorldModel, saveCodeWorldModel } from '@hmharness/cognitive';
17
+ import { syncExtensionRuntime } from "./extension-wm-sensor.js";
18
+ const SAVE_DEBOUNCE_MS = 2_000;
19
+ const entries = new Map();
20
+ /** The onPageRead hook handed to extensionTools — one closure per home. */
21
+ export function pageReadSink(home) {
22
+ return (raw) => {
23
+ void observe(home, raw);
24
+ };
25
+ }
26
+ async function observe(home, raw) {
27
+ try {
28
+ let entry = entries.get(home);
29
+ if (!entry) {
30
+ entry = { load: loadCodeWorldModel(home), dirty: false, timer: null };
31
+ entries.set(home, entry);
32
+ }
33
+ const cwm = await entry.load;
34
+ syncExtensionRuntime(cwm, raw);
35
+ entry.dirty = true;
36
+ if (!entry.timer) {
37
+ entry.timer = setTimeout(() => {
38
+ if (!entry)
39
+ return;
40
+ entry.timer = null;
41
+ void flush(home);
42
+ }, SAVE_DEBOUNCE_MS);
43
+ }
44
+ }
45
+ catch { /* observation is best-effort by contract */ }
46
+ }
47
+ async function flush(home) {
48
+ const entry = entries.get(home);
49
+ if (!entry || !entry.dirty)
50
+ return;
51
+ entry.dirty = false;
52
+ try {
53
+ const cwm = await entry.load;
54
+ await saveCodeWorldModel(home, cwm);
55
+ }
56
+ catch {
57
+ entry.dirty = true; // keep the evidence for the next flush
58
+ }
59
+ }
60
+ /** Test isolation: drop caches + pending timers (never used in production). */
61
+ export function resetCodeWorldRecorder() {
62
+ for (const [, e] of entries)
63
+ if (e.timer)
64
+ clearTimeout(e.timer);
65
+ entries.clear();
66
+ }
67
+ /** Introspection for tests/status surfaces: the live model for a home. */
68
+ export async function liveCodeWorldModel(home) {
69
+ const entry = entries.get(home);
70
+ return entry ? entry.load : null;
71
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @hmharness/agent - LSP → Code World Model sensor wiring (audit line B)
3
+ *
4
+ * "LSP 提供 symbol/definition/reference/... 结构化观察,可作为 Code World
5
+ * Model 的传感器;不要把它做成 IDE" (02 doc). This adapter lives in agent
6
+ * — the composition layer that already depends on BOTH @hmharness/lsp and
7
+ * @hmharness/cognitive — so neither layering direction is violated.
8
+ *
9
+ * Mapping (LSP 3.18 shapes → audit ontology):
10
+ * DocumentSymbol tree → CodeEntity per symbol + 'defines' relation per
11
+ * parent→child edge (the only relation LSP symbols PROVE; calls/
12
+ * references need deeper requests — later sensors)
13
+ * publishDiagnostics payload → CodeDiagnostic (source: 'lsp' — the
14
+ * feedback-not-proof label travels into the model)
15
+ * BuildFact/RuntimeFact have their own sensors (build tool, runs) —
16
+ * not this adapter's job.
17
+ */
18
+ import { CodeWorldModel, type CodeDiagnostic, type CodeEntity, type SymbolRelation } from '@hmharness/cognitive';
19
+ import type { Diagnostic, DocumentSymbol } from '@hmharness/lsp';
20
+ /**
21
+ * LSP DocumentSymbol tree → entities + 'defines' relations. Nested children
22
+ * become defines edges (a document defines its top-level symbols; a class
23
+ * defines its methods). Only what the symbols response PROVES is emitted.
24
+ */
25
+ export declare function lspSymbolsToOntology(uri: string, symbols: DocumentSymbol[]): {
26
+ entities: CodeEntity[];
27
+ relations: SymbolRelation[];
28
+ };
29
+ /** LSP diagnostics payload → audit CodeDiagnostic (source pinned to 'lsp'). */
30
+ export declare function lspDiagnosticsToOntology(uri: string, diagnostics: Diagnostic[]): CodeDiagnostic[];
31
+ export interface CodeWorldSyncInput {
32
+ uri: string;
33
+ symbols?: DocumentSymbol[];
34
+ diagnostics?: Diagnostic[];
35
+ }
36
+ /** One sensor sync: symbols + diagnostics from LSP into the model. */
37
+ export declare function syncCodeWorldModel(cwm: CodeWorldModel, input: CodeWorldSyncInput): {
38
+ entitiesIngested: number;
39
+ relationsIngested: number;
40
+ diagnosticsIngested: number;
41
+ };
@@ -0,0 +1,69 @@
1
+ /** Map a LSP symbol kind number to the audit's entity kind (best effort). */
2
+ function entityKind(symbolKind) {
3
+ // LTS subset of LSP SymbolKind; unknown kinds land as 'field' — honest
4
+ // coarseness beats a wrong specific kind (the audit ontology has no
5
+ // 'property' kind — methods/fields cover the LSP property case)
6
+ if (symbolKind === 12)
7
+ return 'function';
8
+ if (symbolKind === 5)
9
+ return 'class';
10
+ if (symbolKind === 6)
11
+ return 'method';
12
+ if (symbolKind === 2)
13
+ return 'module';
14
+ if (symbolKind === 1)
15
+ return 'file';
16
+ return 'field';
17
+ }
18
+ /**
19
+ * LSP DocumentSymbol tree → entities + 'defines' relations. Nested children
20
+ * become defines edges (a document defines its top-level symbols; a class
21
+ * defines its methods). Only what the symbols response PROVES is emitted.
22
+ */
23
+ export function lspSymbolsToOntology(uri, symbols) {
24
+ const entities = [];
25
+ const relations = [];
26
+ const walk = (list, parentId) => {
27
+ for (const s of list) {
28
+ const id = `${uri}#${s.name}`;
29
+ entities.push({
30
+ id,
31
+ kind: entityKind(s.kind),
32
+ name: s.name,
33
+ uri,
34
+ ...(s.range?.start ? { range: { startLine: s.range.start.line + 1, endLine: (s.range.end?.line ?? s.range.start.line) + 1 } } : {}),
35
+ });
36
+ if (parentId)
37
+ relations.push({ from: parentId, to: id, kind: 'defines' });
38
+ if (s.children?.length)
39
+ walk(s.children, id);
40
+ }
41
+ };
42
+ walk(symbols, null);
43
+ return { entities, relations };
44
+ }
45
+ /** LSP diagnostics payload → audit CodeDiagnostic (source pinned to 'lsp'). */
46
+ export function lspDiagnosticsToOntology(uri, diagnostics) {
47
+ return diagnostics.map((d) => ({
48
+ uri,
49
+ severity: (d.severity ?? 1),
50
+ message: d.message,
51
+ source: 'lsp', // the feedback-not-proof label rides into the model
52
+ }));
53
+ }
54
+ /** One sensor sync: symbols + diagnostics from LSP into the model. */
55
+ export function syncCodeWorldModel(cwm, input) {
56
+ const result = { entitiesIngested: 0, relationsIngested: 0, diagnosticsIngested: 0 };
57
+ if (input.symbols?.length) {
58
+ const { entities, relations } = lspSymbolsToOntology(input.uri, input.symbols);
59
+ cwm.ingest({ entities, relations });
60
+ result.entitiesIngested = entities.length;
61
+ result.relationsIngested = relations.length;
62
+ }
63
+ if (input.diagnostics) {
64
+ const diags = lspDiagnosticsToOntology(input.uri, input.diagnostics);
65
+ cwm.ingest({ diagnostics: diags });
66
+ result.diagnosticsIngested = diags.length;
67
+ }
68
+ return result;
69
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @hmharness/agent - browser-extension → Code World Model runtime sensor
3
+ *
4
+ * The audit ontology's RuntimeFact ("runs" sensor) gets its first source:
5
+ * the user's REAL browser. Every extension_page_read observation lands as
6
+ * runtime facts — where the agent looked and what the user had selected.
7
+ * Like the LSP sensor (code-wm-sensor.ts, round 35), this adapter lives
8
+ * in agent — the composition layer that already depends on BOTH
9
+ * @hmharness/extension and @hmharness/cognitive — so neither layering
10
+ * direction is violated, and the mapping is a library call waiting for
11
+ * the model's persistence wiring (same honest posture as round 35).
12
+ *
13
+ * Mapping discipline (honest, bounded):
14
+ * every page read → ONE RuntimeFact kind 'ext.page.read'
15
+ * (title — url, capped; the identity is the URL)
16
+ * non-empty selection → ONE kind 'ext.page.selection' (user-intent
17
+ * signal — the human highlighted this text while
18
+ * the agent was working)
19
+ * nothing else → headings/links/text are CONTEXT for the loop's
20
+ * turn, not durable facts; flooding the model with
21
+ * per-heading facts would be noise dressed as
22
+ * evidence.
23
+ */
24
+ import { CodeWorldModel, type RuntimeFact } from '@hmharness/cognitive';
25
+ import type { RawPageData } from '@hmharness/extension';
26
+ /** Page observation → audit RuntimeFacts (pure; no DOM, no I/O). */
27
+ export declare function pageReadToRuntimeFacts(raw: RawPageData, at?: string): RuntimeFact[];
28
+ export interface RuntimeSyncResult {
29
+ factsIngested: number;
30
+ }
31
+ /** One sensor sync: extension page observation → runtime facts. */
32
+ export declare function syncExtensionRuntime(cwm: CodeWorldModel, raw: RawPageData): RuntimeSyncResult;
@@ -0,0 +1,21 @@
1
+ const clip = (s, n) => {
2
+ const t = String(s ?? '').replace(/\s+/g, ' ').trim();
3
+ return t.length > n ? t.slice(0, n) : t;
4
+ };
5
+ /** Page observation → audit RuntimeFacts (pure; no DOM, no I/O). */
6
+ export function pageReadToRuntimeFacts(raw, at = new Date().toISOString()) {
7
+ const title = clip(raw.title, 120);
8
+ const url = clip(raw.url, 300);
9
+ const facts = [{ kind: 'ext.page.read', detail: title ? `${title} — ${url}` : url, at }];
10
+ const selection = clip(raw.selection, 200);
11
+ if (selection)
12
+ facts.push({ kind: 'ext.page.selection', detail: selection, at });
13
+ return facts;
14
+ }
15
+ /** One sensor sync: extension page observation → runtime facts. */
16
+ export function syncExtensionRuntime(cwm, raw) {
17
+ const facts = pageReadToRuntimeFacts(raw);
18
+ for (const f of facts)
19
+ cwm.ingest({ runtime: f });
20
+ return { factsIngested: facts.length };
21
+ }
package/dist/i18n.d.ts CHANGED
@@ -139,6 +139,10 @@ export interface Strings {
139
139
  opsHint: string;
140
140
  tuiLongOutputHint: (tool: string, lines: number) => string;
141
141
  tuiUserFolded: (hidden: number) => string;
142
+ /** update briefing trio (T27-v2): start line, completion line, first-launch note */
143
+ tuiUpdating: (v: string) => string;
144
+ tuiUpdated: (v: string) => string;
145
+ tuiBriefing: (v: string, note: string) => string;
142
146
  tuiAutoUpdateFailed: (why: string) => string;
143
147
  tuiEvolveDone: (proposals: number, insights: number, notes: number) => string;
144
148
  tuiPassRate: (pct: string) => string;
package/dist/i18n.js CHANGED
@@ -114,6 +114,9 @@ const zh = {
114
114
  opsHint: '更多操作:/ops scan 立即扫描并生成今日简报 · /ops brief 查看最近简报',
115
115
  tuiLongOutputHint: (tool, lines) => `⎿ ${tool} 输出 ${lines} 行(已折叠) · 按 z 展开最后一条 · Ctrl+T 看全文`,
116
116
  tuiUserFolded: (hidden) => `(…还有 ${hidden} 行已折叠 · Ctrl+T 看全文)`,
117
+ tuiUpdating: (v) => `⤴ 发现新版本 ${v},后台更新中(不打扰当前会话,新版本下次启动生效)`,
118
+ tuiUpdated: (v) => `✓ 已更新到 ${v} · 重启 hmh 后生效`,
119
+ tuiBriefing: (v, note) => `📋 本次更新 ${v}:${note}`,
117
120
  tuiAutoUpdateFailed: (why) => `⤴ 自动更新未能开始(${why}),下次再试;或手动: npm i -g @hmharness/cli@latest`,
118
121
  tuiEvolveDone: (p, i, n) => `evolve 完成: ${p} 提案 · 洞察 ${i} · 记忆 ${n}`,
119
122
  tuiPassRate: (pct) => `pass rate: ${pct}`,
@@ -267,6 +270,9 @@ const en = {
267
270
  opsHint: 'more: /ops scan pull upstream + write today brief · /ops brief latest brief',
268
271
  tuiLongOutputHint: (tool, lines) => `⎿ ${tool} produced ${lines} lines (folded) · z expands the last cell · Ctrl+T full transcript`,
269
272
  tuiUserFolded: (hidden) => `(…${hidden} more lines folded · Ctrl+T full transcript)`,
273
+ tuiUpdating: (v) => `⤴ new version ${v} - updating in the background (this session is unaffected; effective on next launch)`,
274
+ tuiUpdated: (v) => `✓ updated to ${v} · restart hmh to activate`,
275
+ tuiBriefing: (v, note) => `📋 updated ${v}: ${note}`,
270
276
  tuiAutoUpdateFailed: (why) => `⤴ auto-update could not start (${why}); or manually: npm i -g @hmharness/cli@latest`,
271
277
  tuiEvolveDone: (p, i, n) => `evolve done: ${p} proposals · ${i} insights · ${n} notes`,
272
278
  tuiPassRate: (pct) => `pass rate: ${pct}`,
package/dist/runner.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare const spawnBase: {
21
21
  export declare function nativeRegistry(depth: number, opts?: {
22
22
  lsp?: boolean;
23
23
  browser?: boolean;
24
+ extension?: boolean;
24
25
  workspaceRoot?: string;
25
26
  }): Registry;
26
27
  export declare function buildRegistry(opts?: {
package/dist/runner.js CHANGED
@@ -17,6 +17,8 @@ import { harmonyTools } from '@hmharness/domain-harmony';
17
17
  import { opsTools } from '@hmharness/domain-ops';
18
18
  import { discoverServers, lspTools } from '@hmharness/lsp';
19
19
  import { discoverBrowsers, browserTools } from '@hmharness/browser';
20
+ import { discoverExtensionBridgeSync, extensionTools } from '@hmharness/extension';
21
+ import { pageReadSink } from "./code-wm-recorder.js";
20
22
  import * as readline from 'node:readline/promises';
21
23
  import { stdin } from 'node:process';
22
24
  import { baseTools } from "./tools.js";
@@ -65,6 +67,24 @@ export function nativeRegistry(depth, opts = {}) {
65
67
  }
66
68
  catch { /* browser optional: never block tool assembly */ }
67
69
  }
70
+ // extension_* family (2026-10-04, ChatGPT-style browser-extension bridge)
71
+ // — the user's REAL browser via the paired extension. Same dead-tools
72
+ // rule, but liveness is a STATE FILE the bridge keeps fresh (registry
73
+ // build stays sync); registration additionally requires an extension
74
+ // to be CONNECTED right now, and the tools themselves re-check the
75
+ // bridge over HTTP on every call.
76
+ if (opts.extension !== false) {
77
+ try {
78
+ const bridge = discoverExtensionBridgeSync(homeDir());
79
+ if (bridge.healthy && bridge.connected) {
80
+ // onPageRead feeds the PERSISTED Code World Model (round 38):
81
+ // every real-browser page observation becomes durable RuntimeFact
82
+ // evidence — best-effort, never able to fail the tool itself.
83
+ reg.registerAll(extensionTools({ home: homeDir(), onPageRead: pageReadSink(homeDir()) }));
84
+ }
85
+ }
86
+ catch { /* extension optional: never block tool assembly */ }
87
+ }
68
88
  if (depth < MAX_SPAWN_DEPTH) {
69
89
  // blueprint M10: every spawn_agent is governed by the live topology
70
90
  // (role contract + shared budget + immutable multi-agent.jsonl audit);
package/dist/tools.js CHANGED
@@ -493,7 +493,7 @@ export const desktopTypeTool = {
493
493
  */
494
494
  export const browserOpenTool = {
495
495
  name: 'browser_open',
496
- description: 'Open a URL in the user\'s default browser (visible). Then use desktop_screenshot + see_image to see the page, desktop_click/desktop_type to interact. Full workflow: browser_open → desktop_screenshot → see_image (find elements) → desktop_click (click) → desktop_screenshot (verify).',
496
+ description: 'Open a URL in the user\'s default browser (visible). Then use desktop_screenshot + see_image to see the page, desktop_click/desktop_type to interact. Full workflow: browser_open → desktop_screenshot → see_image (find elements) → desktop_click (click) → desktop_screenshot (verify). When running inside a host browser (BrowserOS ACP), this navigates the host\'s active tab instead of opening a new OS window — pair it with browser_snapshot/browser_read.',
497
497
  parameters: {
498
498
  type: 'object',
499
499
  properties: {
@@ -506,6 +506,26 @@ export const browserOpenTool = {
506
506
  const url = String(args.url ?? '').trim();
507
507
  if (!/^https?:\/\//i.test(url))
508
508
  return { output: 'only http(s) URLs', isError: true };
509
+ // Under a host browser (acp-serve/BrowserOS): opening the OS default
510
+ // browser pops a SECOND window per call — the exact bug users see as
511
+ // "a new browser window on every message". Navigate the host's active
512
+ // tab instead; the browser_* family reads/verifies from there.
513
+ const attachPort = Number(process.env.HMH_BROWSER_ATTACH ?? 0);
514
+ if (Number.isInteger(attachPort) && attachPort > 0) {
515
+ try {
516
+ const { CdpBrowser } = await import('@hmharness/browser');
517
+ const c = new CdpBrowser({ port: attachPort });
518
+ if (await c.up()) {
519
+ const tabs = (await c.tabs()).filter((t) => !t.url.startsWith('chrome-extension://'));
520
+ const page = tabs.find((t) => !t.url.startsWith('about:')) ?? tabs[0];
521
+ const tid = page?.targetId ?? (await c.openTab('about:blank'));
522
+ await c.navigate(tid, url);
523
+ const title = String(await c.evaluate(tid, 'document.title').catch(() => ''));
524
+ return { output: `navigated the host browser to ${url} (title: ${title || 'loading'}) — use browser_snapshot/browser_read to see the page` };
525
+ }
526
+ }
527
+ catch { /* fall through to the default-browser path if attach fails */ }
528
+ }
509
529
  try {
510
530
  if (process.platform === 'win32') {
511
531
  const { execFile } = await import('node:child_process');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/agent",
3
- "version": "0.23.21",
3
+ "version": "0.23.23",
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,15 +15,16 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/cognitive": "0.23.21",
19
- "@hmharness/domain-harmony": "0.23.21",
20
- "@hmharness/domain-ops": "0.23.21",
21
- "@hmharness/evolution": "0.23.21",
22
- "@hmharness/kernel": "0.23.21",
23
- "@hmharness/observability": "0.23.21",
24
- "@hmharness/sandbox": "0.23.21",
25
- "@hmharness/lsp": "0.23.21",
26
- "@hmharness/browser": "0.23.21"
18
+ "@hmharness/cognitive": "0.23.23",
19
+ "@hmharness/domain-harmony": "0.23.23",
20
+ "@hmharness/domain-ops": "0.23.23",
21
+ "@hmharness/evolution": "0.23.23",
22
+ "@hmharness/kernel": "0.23.23",
23
+ "@hmharness/observability": "0.23.23",
24
+ "@hmharness/sandbox": "0.23.23",
25
+ "@hmharness/lsp": "0.23.23",
26
+ "@hmharness/browser": "0.23.23",
27
+ "@hmharness/extension": "0.23.23"
27
28
  },
28
29
  "files": [
29
30
  "dist"