@hmharness/agent 0.23.46 → 0.23.48

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.
@@ -10,6 +10,10 @@ const RISK_RULES = [
10
10
  { match: /^(harmony_emulator_(create|start|stop|delete))$/, risk: 'high', permissions: ['device.write', 'process.spawn'], network: true, sideEffects: ['device-mutation'] },
11
11
  { match: /^(harmony_build|harmony_cjpm_build|harmony_cjpm_test|harmony_lint)$/, risk: 'medium', permissions: ['process.spawn', 'fs.write'], network: true, sideEffects: ['build-artifacts'] },
12
12
  { match: /^spawn_agent$/, risk: 'high', permissions: ['agent.spawn'], network: false, sideEffects: ['subagent-run'] },
13
+ // git forges (GitHub/AtomGit): reads are low; writes mutate remote VCS state
14
+ // and are ALL approval-gated (needsApproval) like their domain-ops siblings.
15
+ { match: /^forge_(repo|pr|issue|release)_(create|merge|upload)$|^forge_raw$/, risk: 'high', permissions: ['net.remote', 'fs.read'], network: true, sideEffects: ['remote-vcs'] },
16
+ { match: /^forge_(repo|pr|issue)_list$|^forge_pr_checks$|^forge_status$/, risk: 'low', permissions: ['net.remote'], network: true, sideEffects: [] },
13
17
  ];
14
18
  export function manifestFor(tool) {
15
19
  const rule = RISK_RULES.find((r) => r.match.test(tool.name));
@@ -0,0 +1,81 @@
1
+ import type { Tool, ToolResult } from '@hmharness/kernel';
2
+ export type ForgeHost = 'github' | 'atomgit';
3
+ export declare const FORGE_BINS: Record<ForgeHost, string>;
4
+ export declare function installHint(host: ForgeHost): string;
5
+ export declare function loginHint(host: ForgeHost): string;
6
+ /** Shell-quote one argument. Windows: cmd.exe rules (wrap in "...", double
7
+ * embedded quotes; a literal % inside quotes still expands — accepted as a
8
+ * cmd.exe limitation, forge inputs rarely contain %). POSIX: single quotes
9
+ * with the '\'' escape. (Pure, testable.) */
10
+ export declare function quoteArg(s: string, platform?: string): string;
11
+ /** Parse `auth status` text for BOTH CLIs' phrasings:
12
+ * gh: "✓ Logged in to github.com account swsgbl (keyring)"
13
+ * ag: "✓ Logged in to atomgit.com as demo-user" */
14
+ export declare function parseAuthStatus(text: string): {
15
+ loggedIn: boolean;
16
+ user?: string;
17
+ };
18
+ export declare function repoListArgs(host: ForgeHost, owner?: string, page?: number): string[];
19
+ export declare function repoCreateArgs(host: ForgeHost, name: string, visibility: 'public' | 'private', description?: string): string[];
20
+ export declare function prListArgs(host: ForgeHost, repo: string, state?: string): string[];
21
+ export declare function prCreateArgs(host: ForgeHost, repo: string, head: string, base: string, title: string, body?: string): string[];
22
+ export declare function prMergeArgs(host: ForgeHost, repo: string, num: string, method?: 'merge' | 'squash' | 'rebase'): string[];
23
+ export declare function prChecksArgs(host: ForgeHost, repo: string, num: string): string[];
24
+ export declare function issueListArgs(host: ForgeHost, repo: string, state?: string): string[];
25
+ export declare function issueCreateArgs(host: ForgeHost, repo: string, title: string, body?: string): string[];
26
+ export declare function releaseCreateArgs(host: ForgeHost, repo: string, tag: string, name: string, body: string, target?: string): string[];
27
+ export declare function releaseUploadArgs(host: ForgeHost, repo: string, tag: string, file: string): string[];
28
+ export interface ForgeHostStatus {
29
+ host: ForgeHost;
30
+ bin: string;
31
+ installed: boolean;
32
+ loggedIn: boolean;
33
+ user?: string;
34
+ }
35
+ /** where/which probe — goes through the shell so PATH seams (real or fake) apply. */
36
+ export declare function forgeBinInstalled(host: ForgeHost): Promise<boolean>;
37
+ export declare function forgeDetectHost(host: ForgeHost): Promise<ForgeHostStatus>;
38
+ /** Detect both forges in parallel (forge_status / guidance basis). */
39
+ export declare function forgeDetect(): Promise<ForgeHostStatus[]>;
40
+ /** Run one forge CLI command (quoted, 60s timeout, agent cwd). Installed-check
41
+ * first so a missing CLI yields the install hint, not cmd.exe mojibake.
42
+ * (exec instead of execFile is deliberate: ag installs as ag.cmd, which
43
+ * execFile cannot spawn on Windows since the 2024 security patch; quoteArg
44
+ * hardens every shell metacharacter, so the injection surface is strictly
45
+ * smaller than run_command's free-form line.) */
46
+ export declare function forgeRun(host: ForgeHost, args: string[], opts?: {
47
+ timeoutMs?: number;
48
+ cwd?: string;
49
+ }): Promise<{
50
+ ok: boolean;
51
+ output: string;
52
+ }>;
53
+ /** Tolerant JSON-array extraction: finds the first [ or { (both CLIs may
54
+ * print a banner line before the payload), wraps single objects. */
55
+ export declare function parseJsonOut(text: string): unknown[] | null;
56
+ export declare function renderRepos(rows: unknown[]): string;
57
+ export declare function renderPrs(rows: unknown[]): string;
58
+ export declare function renderIssues(rows: unknown[]): string;
59
+ export declare const forgeStatusTool: Tool;
60
+ export declare const forgeRepoListTool: Tool;
61
+ export declare const forgeRepoCreateTool: Tool;
62
+ export declare const forgePrListTool: Tool;
63
+ export declare const forgePrCreateTool: Tool;
64
+ export declare const forgePrMergeTool: Tool;
65
+ export declare const forgePrChecksTool: Tool;
66
+ export declare const forgeIssueListTool: Tool;
67
+ export declare const forgeIssueCreateTool: Tool;
68
+ export declare const forgeReleaseCreateTool: Tool;
69
+ export declare const forgeReleaseUploadTool: Tool;
70
+ export declare const forgeRawTool: Tool;
71
+ /** Start the web login flow and poll auth status until it succeeds or the
72
+ * deadline passes. The device code / URL (gh prints them; ag opens the
73
+ * browser) go to stderr immediately so terminal frontends show them during
74
+ * the wait, and into the final result either way. */
75
+ export declare function forgeLogin(host: ForgeHost, opts?: {
76
+ pollMs?: number;
77
+ timeoutMs?: number;
78
+ startFlow?: boolean;
79
+ }): Promise<ToolResult>;
80
+ export declare const forgeLoginTool: Tool;
81
+ export declare const forgeTools: Tool[];
package/dist/forge.js ADDED
@@ -0,0 +1,635 @@
1
+ /**
2
+ * @hmharness/agent - git forge tools (GitHub / AtomGit)
3
+ *
4
+ * First-class repository operations on the user's forges, driven by the
5
+ * platforms' own first-party CLIs: gh (GitHub) and ag (AtomGit — a
6
+ * gh-shaped command surface). Zero new dependencies: shell call + JSON
7
+ * parse. Credentials NEVER enter hmharness state — they stay in gh's
8
+ * keyring/hosts.yml and ag's own config storage; we only read `auth
9
+ * status` text to report the login user.
10
+ *
11
+ * Wire differences absorbed here (builders are pure + exported so tests
12
+ * assert exact argument assembly without a forge): gh `--json` takes a
13
+ * field list, ag `--json` is a boolean dump; gh selects repos with
14
+ * `-R owner/repo`, ag takes owner/repo positionally; gh release notes
15
+ * are `--title/--notes`, ag's are `--name/--body`; ag numbers arrive as
16
+ * strings. All commands run through exec() with manual quoting because
17
+ * ag installs as `ag.cmd` (npm global) — execFile cannot spawn .cmd on
18
+ * Windows (ENOENT/EINVAL since the 2024 security patch), and exec()
19
+ * resolves PATH exactly like the user's shell, fake-CLI test seams
20
+ * included.
21
+ */
22
+ import { exec } from 'node:child_process';
23
+ import { isAbsolute, resolve } from 'node:path';
24
+ import { promisify } from 'node:util';
25
+ const execCb = promisify(exec);
26
+ export const FORGE_BINS = { github: 'gh', atomgit: 'ag' };
27
+ const PAGE_SIZE = 30;
28
+ const MAX_PAGE = 10;
29
+ const RUN_TIMEOUT_MS = 60_000;
30
+ export function installHint(host) {
31
+ return host === 'github'
32
+ ? 'gh (GitHub CLI) is not installed. Install it first: `winget install GitHub.cli` on Windows, `brew install gh` on macOS — https://cli.github.com'
33
+ : 'ag (AtomGit CLI) is not installed. Install it first: `npm install -g @hust-open-atom-club/atomgit-cli` — https://atomgit.com/hust-open-atom-club/atomgit-cli';
34
+ }
35
+ export function loginHint(host) {
36
+ const bin = FORGE_BINS[host];
37
+ return `Not logged in to ${host}. Run the forge_login tool with host:"${host}" (it shows the device code/URL), or manually: \`${bin} auth login\``;
38
+ }
39
+ /** Shell-quote one argument. Windows: cmd.exe rules (wrap in "...", double
40
+ * embedded quotes; a literal % inside quotes still expands — accepted as a
41
+ * cmd.exe limitation, forge inputs rarely contain %). POSIX: single quotes
42
+ * with the '\'' escape. (Pure, testable.) */
43
+ export function quoteArg(s, platform = process.platform) {
44
+ if (s === '')
45
+ return '""';
46
+ if (platform === 'win32') {
47
+ if (!/[\s"&|<>^()%!]/.test(s))
48
+ return s;
49
+ return '"' + s.replace(/"/g, '""') + '"';
50
+ }
51
+ if (!/[\s'"$`\\&|;<>()*?\[\]#!]/.test(s))
52
+ return s;
53
+ return "'" + s.replace(/'/g, "'\\''") + "'";
54
+ }
55
+ /** Parse `auth status` text for BOTH CLIs' phrasings:
56
+ * gh: "✓ Logged in to github.com account swsgbl (keyring)"
57
+ * ag: "✓ Logged in to atomgit.com as demo-user" */
58
+ export function parseAuthStatus(text) {
59
+ if (!/Logged in to/.test(text))
60
+ return { loggedIn: false };
61
+ const m = /Logged in to \S+ (?:account|as) (\S+?)[\s(\r]/.exec(text) ?? /Logged in to \S+ (?:account|as) (\S+)/.exec(text);
62
+ return { loggedIn: true, user: m?.[1] };
63
+ }
64
+ function str(v) { return v === undefined || v === null ? '' : String(v); }
65
+ // ---------------- pure command builders ----------------
66
+ export function repoListArgs(host, owner, page = 1) {
67
+ // neither CLI supports --page/--offset: page N fetches N*30 rows and the
68
+ // runner slices the window out (honest paging for N<=10 without --paginate)
69
+ const p = pageOf(page);
70
+ return [
71
+ 'repo', 'list',
72
+ ...(owner ? [owner] : []),
73
+ '--limit', String(p * PAGE_SIZE),
74
+ ...(host === 'github' ? ['--json', 'name,visibility,updatedAt,pushedAt'] : ['--json']),
75
+ ];
76
+ }
77
+ function pageOf(page) {
78
+ const p = Math.floor(Number(page) || 1);
79
+ return Math.max(1, Math.min(p, MAX_PAGE));
80
+ }
81
+ export function repoCreateArgs(host, name, visibility, description) {
82
+ const desc = description ? [host === 'github' ? '--description' : '-d', description] : [];
83
+ return ['repo', 'create', name, visibility === 'private' ? '--private' : '--public', ...desc];
84
+ }
85
+ export function prListArgs(host, repo, state = 'open') {
86
+ const s = ['open', 'closed', 'all'].includes(state) ? state : 'open';
87
+ return host === 'github'
88
+ ? ['pr', 'list', '-R', repo, '--state', s, '--limit', '30', '--json', 'number,title,state,author,headRefName,url']
89
+ : ['pr', 'list', repo, '--state', s, '--limit', '30', '--json'];
90
+ }
91
+ export function prCreateArgs(host, repo, head, base, title, body) {
92
+ const b = body ? ['--body', body] : [];
93
+ const sel = host === 'github' ? ['-R', repo] : [repo];
94
+ return ['pr', 'create', ...sel, '--head', head, '--base', base, '--title', title, ...b];
95
+ }
96
+ export function prMergeArgs(host, repo, num, method = 'merge') {
97
+ if (host === 'github') {
98
+ const flag = method === 'squash' ? '--squash' : method === 'rebase' ? '--rebase' : '--merge';
99
+ return ['pr', 'merge', num, '-R', repo, flag];
100
+ }
101
+ // ag merges with the default method unless a flag says otherwise
102
+ const flag = method === 'squash' ? ['--squash'] : method === 'rebase' ? ['--rebase'] : [];
103
+ return ['pr', 'merge', repo, num, ...flag];
104
+ }
105
+ export function prChecksArgs(host, repo, num) {
106
+ return host === 'github'
107
+ ? ['pr', 'checks', num, '-R', repo, '--json', 'name,state']
108
+ : ['pr', 'checks', repo, num];
109
+ }
110
+ export function issueListArgs(host, repo, state = 'open') {
111
+ const s = ['open', 'closed', 'all'].includes(state) ? state : 'open';
112
+ return host === 'github'
113
+ ? ['issue', 'list', '-R', repo, '--state', s, '--limit', '30', '--json', 'number,title,state,author,url']
114
+ : ['issue', 'list', repo, '--state', s, '--limit', '30', '--json'];
115
+ }
116
+ export function issueCreateArgs(host, repo, title, body) {
117
+ const b = body ? ['--body', body] : [];
118
+ const sel = host === 'github' ? ['-R', repo] : [repo];
119
+ return ['issue', 'create', ...sel, '--title', title, ...b];
120
+ }
121
+ export function releaseCreateArgs(host, repo, tag, name, body, target) {
122
+ const t = target ? ['--target', target] : [];
123
+ return host === 'github'
124
+ ? ['release', 'create', tag, '-R', repo, '--title', name, '--notes', body, ...t]
125
+ : ['release', 'create', repo, tag, '--name', name, '--body', body, ...t]; // AtomGit API requires a non-empty body
126
+ }
127
+ export function releaseUploadArgs(host, repo, tag, file) {
128
+ return host === 'github'
129
+ ? ['release', 'upload', tag, file, '-R', repo]
130
+ : ['release', 'upload', repo, tag, file];
131
+ }
132
+ /** where/which probe — goes through the shell so PATH seams (real or fake) apply. */
133
+ export async function forgeBinInstalled(host) {
134
+ const bin = FORGE_BINS[host];
135
+ const probe = process.platform === 'win32' ? `where ${bin}` : `command -v ${bin}`;
136
+ try {
137
+ await execCb(probe, { timeout: 10_000, windowsHide: true });
138
+ return true;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ export async function forgeDetectHost(host) {
145
+ const bin = FORGE_BINS[host];
146
+ if (!(await forgeBinInstalled(host)))
147
+ return { host, bin, installed: false, loggedIn: false };
148
+ try {
149
+ const { stdout, stderr } = await execCb(`${bin} auth status`, { timeout: 15_000, windowsHide: true });
150
+ const p = parseAuthStatus(stdout + '\n' + stderr);
151
+ return { host, bin, installed: true, ...p };
152
+ }
153
+ catch (err) {
154
+ // both CLIs exit non-zero when logged out but still print their status
155
+ const e = err;
156
+ const p = parseAuthStatus([e.stdout, e.stderr].filter(Boolean).join('\n'));
157
+ return { host, bin, installed: true, ...p };
158
+ }
159
+ }
160
+ /** Detect both forges in parallel (forge_status / guidance basis). */
161
+ export async function forgeDetect() {
162
+ return Promise.all(['github', 'atomgit'].map(forgeDetectHost));
163
+ }
164
+ /** Run one forge CLI command (quoted, 60s timeout, agent cwd). Installed-check
165
+ * first so a missing CLI yields the install hint, not cmd.exe mojibake.
166
+ * (exec instead of execFile is deliberate: ag installs as ag.cmd, which
167
+ * execFile cannot spawn on Windows since the 2024 security patch; quoteArg
168
+ * hardens every shell metacharacter, so the injection surface is strictly
169
+ * smaller than run_command's free-form line.) */
170
+ export async function forgeRun(host, args, opts = {}) {
171
+ const bin = FORGE_BINS[host];
172
+ if (!(await forgeBinInstalled(host)))
173
+ return { ok: false, output: installHint(host) };
174
+ const line = [bin, ...args.map((a) => quoteArg(String(a)))].join(' ');
175
+ try {
176
+ const { stdout, stderr } = await execCb(line, {
177
+ cwd: opts.cwd, timeout: opts.timeoutMs ?? RUN_TIMEOUT_MS, windowsHide: true, maxBuffer: 8 * 1024 * 1024,
178
+ });
179
+ const out = (stdout || '').trim() + (stderr && stderr.trim() ? `\n[stderr]\n${stderr.trim()}` : '');
180
+ return { ok: true, output: out.trim() || '(no output)' };
181
+ }
182
+ catch (err) {
183
+ const e = err;
184
+ if (e.killed)
185
+ return { ok: false, output: `${bin} command timed out after ${opts.timeoutMs ?? RUN_TIMEOUT_MS}ms — narrow it (add --limit, a smaller page) and retry.` };
186
+ const out = [e.stdout, e.stderr, e.message].filter(Boolean).map((s) => String(s).trim()).filter(Boolean).join('\n');
187
+ return { ok: false, output: out.slice(0, 20_000) || `${bin} failed (no output)` };
188
+ }
189
+ }
190
+ /** Guard shared by every forge tool except status/login/raw: valid host,
191
+ * CLI installed, logged in — or the honest, actionable error. */
192
+ async function guard(host) {
193
+ const st = await forgeDetectHost(host);
194
+ if (!st.installed)
195
+ return { output: installHint(host), isError: true };
196
+ if (!st.loggedIn)
197
+ return { output: loginHint(host), isError: true };
198
+ return null;
199
+ }
200
+ function hostOf(args) {
201
+ const h = String(args.host ?? '');
202
+ if (h === 'github' || h === 'atomgit')
203
+ return { host: h };
204
+ return { err: { output: `host must be 'github' or 'atomgit' (got '${h}')`, isError: true } };
205
+ }
206
+ // ---------------- renderers (forge JSON -> compact lines) ----------------
207
+ /** Tolerant JSON-array extraction: finds the first [ or { (both CLIs may
208
+ * print a banner line before the payload), wraps single objects. */
209
+ export function parseJsonOut(text) {
210
+ const idxs = ['[', '{'].map((c) => text.indexOf(c)).filter((i) => i >= 0);
211
+ if (!idxs.length)
212
+ return null;
213
+ try {
214
+ const v = JSON.parse(text.slice(Math.min(...idxs)));
215
+ return Array.isArray(v) ? v : [v];
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ export function renderRepos(rows) {
222
+ const lines = rows.map((r) => {
223
+ const o = r;
224
+ const name = str(o.name) || str(o.fullName) || '?';
225
+ const vis = (str(o.visibility) || (o.isPrivate ? 'private' : '')).toLowerCase();
226
+ // AtomGit's repo payload has no pushedAt — say so instead of inventing one
227
+ const pushed = o.pushedAt ? str(o.pushedAt) : 'n/a (host does not report)';
228
+ return `${name} [${vis.includes('priv') ? 'private' : 'public'}] updated=${str(o.updatedAt) || '?'} pushed=${pushed}`;
229
+ });
230
+ return lines.length ? lines.join('\n') : '(no repositories found)';
231
+ }
232
+ export function renderPrs(rows) {
233
+ const lines = rows.map((r) => {
234
+ const o = r;
235
+ const author = typeof o.author === 'object' && o.author ? str(o.author.login) : str(o.author);
236
+ const head = str(o.headRefName) || str(o.head);
237
+ return `#${str(o.number)} [${str(o.state) || '?'}] ${str(o.title)} — ${author || '?'}${head ? ` (${head})` : ''} ${str(o.url)}`.trimEnd();
238
+ });
239
+ return lines.length ? lines.join('\n') : '(no pull requests found)';
240
+ }
241
+ export function renderIssues(rows) {
242
+ const lines = rows.map((r) => {
243
+ const o = r;
244
+ const author = typeof o.author === 'object' && o.author ? str(o.author.login) : str(o.author);
245
+ return `#${str(o.number)} [${str(o.state) || '?'}] ${str(o.title)} — ${author || '?'} ${str(o.url)}`.trimEnd();
246
+ });
247
+ return lines.length ? lines.join('\n') : '(no issues found)';
248
+ }
249
+ // ---------------- tools ----------------
250
+ export const forgeStatusTool = {
251
+ name: 'forge_status',
252
+ description: 'Show GitHub (gh) and AtomGit (ag) CLI install + login status at a glance. Call this FIRST for any repository task on a forge.',
253
+ parameters: { type: 'object', properties: {}, required: [] },
254
+ async execute() {
255
+ const rows = await forgeDetect();
256
+ const lines = rows.map((s) => {
257
+ if (!s.installed)
258
+ return `${s.host}: NOT installed — ${installHint(s.host)}`;
259
+ if (!s.loggedIn)
260
+ return `${s.host}: installed (${s.bin}), NOT logged in — ${loginHint(s.host)}`;
261
+ return `${s.host}: installed (${s.bin}), logged in as ${s.user ?? '(user unknown)'}`;
262
+ });
263
+ return { output: lines.join('\n') };
264
+ },
265
+ };
266
+ export const forgeRepoListTool = {
267
+ name: 'forge_repo_list',
268
+ description: 'List repositories on a forge — your own, or an owner/org\'s. host: github|atomgit; optional owner, page (30/page, max 10).',
269
+ parameters: {
270
+ type: 'object',
271
+ properties: {
272
+ host: { type: 'string', description: "'github' or 'atomgit'" },
273
+ owner: { type: 'string', description: 'owner/org login (default: the logged-in account)' },
274
+ page: { type: 'number', description: '1-based page of 30 (max 10)' },
275
+ },
276
+ required: ['host'],
277
+ },
278
+ async execute(args, ctx) {
279
+ const { host, err } = hostOf(args);
280
+ if (!host || err)
281
+ return err;
282
+ const g = await guard(host);
283
+ if (g)
284
+ return g;
285
+ const page = pageOf(Number(args.page ?? 1));
286
+ const r = await forgeRun(host, repoListArgs(host, args.owner ? String(args.owner) : undefined, page), { cwd: ctx.cwd });
287
+ if (!r.ok)
288
+ return { output: r.output, isError: true };
289
+ const rows = parseJsonOut(r.output);
290
+ if (!rows)
291
+ return { output: `could not parse ${host} repo list JSON — raw output:\n${r.output.slice(0, 4_000)}`, isError: true };
292
+ const win = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
293
+ return { output: `${host} repositories (page ${page}${args.owner ? `, owner ${args.owner}` : ''}):\n${renderRepos(win)}` };
294
+ },
295
+ };
296
+ export const forgeRepoCreateTool = {
297
+ name: 'forge_repo_create',
298
+ description: 'Create a repository on a forge. host: github|atomgit; name (may be owner/name to create under an org); visibility public|private (default private); optional description.',
299
+ parameters: {
300
+ type: 'object',
301
+ properties: {
302
+ host: { type: 'string', description: "'github' or 'atomgit'" },
303
+ name: { type: 'string', description: 'repository name (or owner/name)' },
304
+ visibility: { type: 'string', description: "'public' or 'private' (default private)" },
305
+ description: { type: 'string', description: 'one-line description' },
306
+ },
307
+ required: ['host', 'name'],
308
+ },
309
+ needsApproval: () => true,
310
+ async execute(args, ctx) {
311
+ const { host, err } = hostOf(args);
312
+ if (!host || err)
313
+ return err;
314
+ const g = await guard(host);
315
+ if (g)
316
+ return g;
317
+ const vis = String(args.visibility ?? 'private') === 'public' ? 'public' : 'private';
318
+ const r = await forgeRun(host, repoCreateArgs(host, String(args.name ?? ''), vis, args.description ? String(args.description) : undefined), { cwd: ctx.cwd });
319
+ return { output: `created ${vis} repo ${args.name} on ${host}:\n${r.output}`, isError: !r.ok };
320
+ },
321
+ };
322
+ export const forgePrListTool = {
323
+ name: 'forge_pr_list',
324
+ description: 'List pull requests of owner/repo on a forge. host: github|atomgit; repo owner/repo; state open|closed|all (default open).',
325
+ parameters: {
326
+ type: 'object',
327
+ properties: {
328
+ host: { type: 'string', description: "'github' or 'atomgit'" },
329
+ repo: { type: 'string', description: 'owner/repo' },
330
+ state: { type: 'string', description: 'open | closed | all (default open)' },
331
+ },
332
+ required: ['host', 'repo'],
333
+ },
334
+ async execute(args, ctx) {
335
+ const { host, err } = hostOf(args);
336
+ if (!host || err)
337
+ return err;
338
+ const g = await guard(host);
339
+ if (g)
340
+ return g;
341
+ const r = await forgeRun(host, prListArgs(host, String(args.repo ?? ''), String(args.state ?? 'open')), { cwd: ctx.cwd });
342
+ if (!r.ok)
343
+ return { output: r.output, isError: true };
344
+ const rows = parseJsonOut(r.output);
345
+ if (!rows)
346
+ return { output: `could not parse ${host} pr list JSON — raw output:\n${r.output.slice(0, 4_000)}`, isError: true };
347
+ return { output: `${host} pull requests (${args.repo}, ${String(args.state ?? 'open')}):\n${renderPrs(rows)}` };
348
+ },
349
+ };
350
+ export const forgePrCreateTool = {
351
+ name: 'forge_pr_create',
352
+ description: 'Create a pull request. host: github|atomgit; repo owner/repo; head branch; base branch; title; optional body. Branch must already be pushed.',
353
+ parameters: {
354
+ type: 'object',
355
+ properties: {
356
+ host: { type: 'string', description: "'github' or 'atomgit'" },
357
+ repo: { type: 'string', description: 'owner/repo' },
358
+ head: { type: 'string', description: 'source branch (e.g. feat/x)' },
359
+ base: { type: 'string', description: 'target branch (e.g. main)' },
360
+ title: { type: 'string', description: 'PR title' },
361
+ body: { type: 'string', description: 'PR description (markdown)' },
362
+ },
363
+ required: ['host', 'repo', 'head', 'base', 'title'],
364
+ },
365
+ needsApproval: () => true,
366
+ async execute(args, ctx) {
367
+ const { host, err } = hostOf(args);
368
+ if (!host || err)
369
+ return err;
370
+ const g = await guard(host);
371
+ if (g)
372
+ return g;
373
+ const r = await forgeRun(host, prCreateArgs(host, String(args.repo ?? ''), String(args.head ?? ''), String(args.base ?? ''), String(args.title ?? ''), args.body ? String(args.body) : undefined), { cwd: ctx.cwd });
374
+ return { output: `pull request created on ${host}:\n${r.output}`, isError: !r.ok };
375
+ },
376
+ };
377
+ export const forgePrMergeTool = {
378
+ name: 'forge_pr_merge',
379
+ description: 'Merge a pull request. host: github|atomgit; repo owner/repo; num PR number; method merge|squash|rebase (default merge). Irreversible — confirm with the user first.',
380
+ parameters: {
381
+ type: 'object',
382
+ properties: {
383
+ host: { type: 'string', description: "'github' or 'atomgit'" },
384
+ repo: { type: 'string', description: 'owner/repo' },
385
+ num: { type: 'string', description: 'PR number' },
386
+ method: { type: 'string', description: 'merge | squash | rebase (default merge)' },
387
+ },
388
+ required: ['host', 'repo', 'num'],
389
+ },
390
+ needsApproval: () => true,
391
+ async execute(args, ctx) {
392
+ const { host, err } = hostOf(args);
393
+ if (!host || err)
394
+ return err;
395
+ const g = await guard(host);
396
+ if (g)
397
+ return g;
398
+ const m = String(args.method ?? 'merge');
399
+ const r = await forgeRun(host, prMergeArgs(host, String(args.repo ?? ''), String(args.num ?? ''), m === 'squash' ? 'squash' : m === 'rebase' ? 'rebase' : 'merge'), { cwd: ctx.cwd });
400
+ return { output: r.output, isError: !r.ok };
401
+ },
402
+ };
403
+ export const forgePrChecksTool = {
404
+ name: 'forge_pr_checks',
405
+ description: 'Show CI checks/status for a pull request. host: github|atomgit; repo owner/repo; num PR number. Older ag builds without `pr checks` report it honestly.',
406
+ parameters: {
407
+ type: 'object',
408
+ properties: {
409
+ host: { type: 'string', description: "'github' or 'atomgit'" },
410
+ repo: { type: 'string', description: 'owner/repo' },
411
+ num: { type: 'string', description: 'PR number' },
412
+ },
413
+ required: ['host', 'repo', 'num'],
414
+ },
415
+ async execute(args, ctx) {
416
+ const { host, err } = hostOf(args);
417
+ if (!host || err)
418
+ return err;
419
+ const g = await guard(host);
420
+ if (g)
421
+ return g;
422
+ const r = await forgeRun(host, prChecksArgs(host, String(args.repo ?? ''), String(args.num ?? '')), { cwd: ctx.cwd });
423
+ if (r.ok) {
424
+ const rows = parseJsonOut(r.output); // gh --json name,state
425
+ const body = rows && rows.length && typeof rows[0]?.name === 'string'
426
+ ? rows.map((x) => { const o = x; return `${str(o.name)}: ${str(o.state)}`; }).join('\n')
427
+ : r.output; // ag prints a human table — pass it through as-is
428
+ return { output: `checks for ${host} PR #${args.num}:\n${body}` };
429
+ }
430
+ return {
431
+ output: `${r.output}\n(If your ag version predates \`ag pr checks\` — added in atomgit-cli 0.7.x — the host genuinely does not expose checks here; tell the user that instead of guessing.)`,
432
+ isError: true,
433
+ };
434
+ },
435
+ };
436
+ export const forgeIssueListTool = {
437
+ name: 'forge_issue_list',
438
+ description: 'List issues of owner/repo on a forge. host: github|atomgit; repo owner/repo; state open|closed|all (default open).',
439
+ parameters: {
440
+ type: 'object',
441
+ properties: {
442
+ host: { type: 'string', description: "'github' or 'atomgit'" },
443
+ repo: { type: 'string', description: 'owner/repo' },
444
+ state: { type: 'string', description: 'open | closed | all (default open)' },
445
+ },
446
+ required: ['host', 'repo'],
447
+ },
448
+ async execute(args, ctx) {
449
+ const { host, err } = hostOf(args);
450
+ if (!host || err)
451
+ return err;
452
+ const g = await guard(host);
453
+ if (g)
454
+ return g;
455
+ const r = await forgeRun(host, issueListArgs(host, String(args.repo ?? ''), String(args.state ?? 'open')), { cwd: ctx.cwd });
456
+ if (!r.ok)
457
+ return { output: r.output, isError: true };
458
+ const rows = parseJsonOut(r.output);
459
+ if (!rows)
460
+ return { output: `could not parse ${host} issue list JSON — raw output:\n${r.output.slice(0, 4_000)}`, isError: true };
461
+ return { output: `${host} issues (${args.repo}, ${String(args.state ?? 'open')}):\n${renderIssues(rows)}` };
462
+ },
463
+ };
464
+ export const forgeIssueCreateTool = {
465
+ name: 'forge_issue_create',
466
+ description: 'Create an issue. host: github|atomgit; repo owner/repo; title; optional body.',
467
+ parameters: {
468
+ type: 'object',
469
+ properties: {
470
+ host: { type: 'string', description: "'github' or 'atomgit'" },
471
+ repo: { type: 'string', description: 'owner/repo' },
472
+ title: { type: 'string', description: 'issue title' },
473
+ body: { type: 'string', description: 'issue body (markdown)' },
474
+ },
475
+ required: ['host', 'repo', 'title'],
476
+ },
477
+ needsApproval: () => true,
478
+ async execute(args, ctx) {
479
+ const { host, err } = hostOf(args);
480
+ if (!host || err)
481
+ return err;
482
+ const g = await guard(host);
483
+ if (g)
484
+ return g;
485
+ const r = await forgeRun(host, issueCreateArgs(host, String(args.repo ?? ''), String(args.title ?? ''), args.body ? String(args.body) : undefined), { cwd: ctx.cwd });
486
+ return { output: `issue created on ${host}:\n${r.output}`, isError: !r.ok };
487
+ },
488
+ };
489
+ export const forgeReleaseCreateTool = {
490
+ name: 'forge_release_create',
491
+ description: 'Create a release from a tag. host: github|atomgit; repo owner/repo; tag; name (release title); body (release notes — required, AtomGit rejects empty); optional target branch/SHA when the tag does not exist yet.',
492
+ parameters: {
493
+ type: 'object',
494
+ properties: {
495
+ host: { type: 'string', description: "'github' or 'atomgit'" },
496
+ repo: { type: 'string', description: 'owner/repo' },
497
+ tag: { type: 'string', description: 'tag name, e.g. v1.2.0' },
498
+ name: { type: 'string', description: 'release title' },
499
+ body: { type: 'string', description: 'release notes (required)' },
500
+ target: { type: 'string', description: 'target branch or commit SHA (creates the tag if absent)' },
501
+ },
502
+ required: ['host', 'repo', 'tag', 'name', 'body'],
503
+ },
504
+ needsApproval: () => true,
505
+ async execute(args, ctx) {
506
+ const { host, err } = hostOf(args);
507
+ if (!host || err)
508
+ return err;
509
+ const g = await guard(host);
510
+ if (g)
511
+ return g;
512
+ if (!String(args.body ?? '').trim())
513
+ return { output: 'body (release notes) is required — AtomGit rejects an empty body and gh would open an interactive editor.', isError: true };
514
+ const r = await forgeRun(host, releaseCreateArgs(host, String(args.repo ?? ''), String(args.tag ?? ''), String(args.name ?? ''), String(args.body), args.target ? String(args.target) : undefined), { cwd: ctx.cwd });
515
+ return { output: `release ${args.tag} created on ${host}:\n${r.output}`, isError: !r.ok };
516
+ },
517
+ };
518
+ export const forgeReleaseUploadTool = {
519
+ name: 'forge_release_upload',
520
+ description: 'Upload a file as an asset of an existing release. host: github|atomgit; repo owner/repo; tag; file path (must exist; its full path, target repo and tag are shown on the approval card — uploading moves data OFF this machine).',
521
+ parameters: {
522
+ type: 'object',
523
+ properties: {
524
+ host: { type: 'string', description: "'github' or 'atomgit'" },
525
+ repo: { type: 'string', description: 'owner/repo' },
526
+ tag: { type: 'string', description: 'release tag' },
527
+ file: { type: 'string', description: 'local file path to upload' },
528
+ },
529
+ required: ['host', 'repo', 'tag', 'file'],
530
+ },
531
+ needsApproval: () => true,
532
+ async execute(args, ctx) {
533
+ const { host, err } = hostOf(args);
534
+ if (!host || err)
535
+ return err;
536
+ const g = await guard(host);
537
+ if (g)
538
+ return g;
539
+ const file = isAbsolute(String(args.file ?? '')) ? String(args.file ?? '') : resolve(ctx.cwd, String(args.file ?? ''));
540
+ try {
541
+ await import('node:fs/promises').then((f) => f.access(file));
542
+ }
543
+ catch {
544
+ return { output: `file not found: ${file}`, isError: true };
545
+ }
546
+ const r = await forgeRun(host, releaseUploadArgs(host, String(args.repo ?? ''), String(args.tag ?? ''), file), { cwd: ctx.cwd });
547
+ return { output: r.output, isError: !r.ok };
548
+ },
549
+ };
550
+ export const forgeRawTool = {
551
+ name: 'forge_raw',
552
+ description: 'DANGEROUS: pass raw arguments straight to gh/ag (host selects the binary). Only for operations the dedicated forge_* tools cannot express. Approval-gated; YOLO/auto modes pass it through.',
553
+ parameters: {
554
+ type: 'object',
555
+ properties: {
556
+ host: { type: 'string', description: "'github' or 'atomgit'" },
557
+ args: { type: 'array', items: { type: 'string' }, description: 'raw CLI arguments, e.g. ["api","user"]' },
558
+ },
559
+ required: ['host', 'args'],
560
+ },
561
+ needsApproval: () => true,
562
+ async execute(args, ctx) {
563
+ const { host, err } = hostOf(args);
564
+ if (!host || err)
565
+ return err;
566
+ if (!Array.isArray(args.args) || !args.args.length)
567
+ return { output: 'args must be a non-empty array of strings', isError: true };
568
+ const r = await forgeRun(host, args.args.map(String), { cwd: ctx.cwd });
569
+ return { output: `[${FORGE_BINS[host]} ${args.args.map(String).join(' ')}]\n${r.output}`, isError: !r.ok };
570
+ },
571
+ };
572
+ // ---------------- login guidance ----------------
573
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
574
+ /** Start the web login flow and poll auth status until it succeeds or the
575
+ * deadline passes. The device code / URL (gh prints them; ag opens the
576
+ * browser) go to stderr immediately so terminal frontends show them during
577
+ * the wait, and into the final result either way. */
578
+ export async function forgeLogin(host, opts = {}) {
579
+ const bin = FORGE_BINS[host];
580
+ const timeoutMs = opts.timeoutMs ?? 180_000;
581
+ const pollMs = opts.pollMs ?? 4_000;
582
+ const already = await forgeDetectHost(host);
583
+ if (!already.installed)
584
+ return { output: installHint(host), isError: true };
585
+ if (already.loggedIn)
586
+ return { output: `already logged in to ${host} as ${already.user ?? '(user unknown)'} — nothing to do.` };
587
+ let early = '';
588
+ if (opts.startFlow !== false) {
589
+ const flow = host === 'github' ? `${bin} auth login --web -h github.com --skip-ssh-key` : `${bin} auth login`;
590
+ const child = exec(flow, { timeout: timeoutMs + 30_000, windowsHide: true }, () => undefined);
591
+ let buf = '';
592
+ const grab = (d) => { buf += String(d); };
593
+ child.stdout?.on('data', grab);
594
+ child.stderr?.on('data', grab);
595
+ await sleep(Math.min(6_000, Math.max(pollMs * 2, 500))); // give the CLI a moment to print the code/URL
596
+ const code = /\b([A-Z0-9]{4,8}-[A-Z0-9]{4,8})\b/.exec(buf)?.[1];
597
+ const url = /https:\/\/\S+/.exec(buf)?.[0];
598
+ early = [code ? `device code: ${code}` : '', url ? `authorization URL: ${url}` : ''].filter(Boolean).join('\n');
599
+ if (early)
600
+ console.error(`[forge_login:${host}] ${early} — complete it in the browser; waiting for the login to land...`);
601
+ }
602
+ const deadline = Date.now() + timeoutMs;
603
+ while (Date.now() < deadline) {
604
+ await sleep(pollMs);
605
+ const st = await forgeDetectHost(host);
606
+ if (st.loggedIn)
607
+ return { output: `login complete: ${host} as ${st.user ?? '(user unknown)'}.${early ? `\n(${early})` : ''}` };
608
+ }
609
+ return {
610
+ output: `login did not complete within ${Math.round(timeoutMs / 1000)}s.${early ? `\n${early}\nFinish it in the browser, then confirm with forge_status.` : `\nNo device code/URL was captured — start it manually: \`${bin} auth login\`, then confirm with forge_status.`}`,
611
+ isError: true,
612
+ };
613
+ }
614
+ export const forgeLoginTool = {
615
+ name: 'forge_login',
616
+ description: 'Guide the user through logging in to a forge (host: github|atomgit): starts `gh auth login --web` / `ag auth login` (the user completes it in the browser), shows the device code/URL, and polls until logged in (3 min timeout).',
617
+ parameters: {
618
+ type: 'object',
619
+ properties: { host: { type: 'string', description: "'github' or 'atomgit'" } },
620
+ required: ['host'],
621
+ },
622
+ async execute(args, ctx) {
623
+ const { host, err } = hostOf(args);
624
+ if (!host || err)
625
+ return err;
626
+ return forgeLogin(host);
627
+ },
628
+ };
629
+ export const forgeTools = [
630
+ forgeStatusTool, forgeRepoListTool, forgeRepoCreateTool,
631
+ forgePrListTool, forgePrCreateTool, forgePrMergeTool, forgePrChecksTool,
632
+ forgeIssueListTool, forgeIssueCreateTool,
633
+ forgeReleaseCreateTool, forgeReleaseUploadTool,
634
+ forgeRawTool, forgeLoginTool,
635
+ ];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { baseTools, readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool } from './tools.js';
2
+ export { forgeTools, forgeDetect, forgeDetectHost, forgeRun, forgeLogin, quoteArg, parseAuthStatus, parseJsonOut, renderRepos, renderPrs, renderIssues, repoListArgs, repoCreateArgs, prListArgs, prCreateArgs, prMergeArgs, prChecksArgs, issueListArgs, issueCreateArgs, releaseCreateArgs, releaseUploadArgs, FORGE_BINS, installHint, loginHint, type ForgeHost, type ForgeHostStatus, } from './forge.js';
2
3
  export { manifestFor, capabilityReport, authorize, type CapabilityManifest, type CapabilityRisk, type PolicyMode } from './capability.js';
3
4
  export { checkpointProject, createProject, findProject, interruptProject, listProjects, loadProject, projectFor, releaseProject, restoreCheckpoint, resumeBundle, transitionProject, attachRun, newProjectId, type CheckpointRef, type DecisionEntry, type ProjectRecord, type ProjectState, } from './project.js';
4
5
  export { mechanicalGate, parseVerdict, runPipeline, type PipelineOptions, type PipelineReport, type StageRecord, type StageRole, } from './pipeline.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { baseTools, readFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool } from "./tools.js";
2
+ export { forgeTools, forgeDetect, forgeDetectHost, forgeRun, forgeLogin, quoteArg, parseAuthStatus, parseJsonOut, renderRepos, renderPrs, renderIssues, repoListArgs, repoCreateArgs, prListArgs, prCreateArgs, prMergeArgs, prChecksArgs, issueListArgs, issueCreateArgs, releaseCreateArgs, releaseUploadArgs, FORGE_BINS, installHint, loginHint, } from "./forge.js";
2
3
  export { manifestFor, capabilityReport, authorize } from "./capability.js";
3
4
  export { checkpointProject, createProject, findProject, interruptProject, listProjects, loadProject, projectFor, releaseProject, restoreCheckpoint, resumeBundle, transitionProject, attachRun, newProjectId, } from "./project.js";
4
5
  export { mechanicalGate, parseVerdict, runPipeline, } from "./pipeline.js";
package/dist/prompt.js CHANGED
@@ -28,6 +28,8 @@ export function buildSystemPrompt(opts) {
28
28
  '', '## 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>`).',
29
29
  // ---- 鸿蒙域 ----
30
30
  '', '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.',
31
+ // ---- Git forge 域(GitHub / AtomGit,2026-10-30 用户特性) ----
32
+ '', '## Git forges (GitHub / AtomGit)', 'Repository operations on GitHub or AtomGit — listing/creating repos, PRs, issues, releases, checking CI — go through the forge_* tools (host: "github" → gh CLI, "atomgit" → ag CLI). Do NOT send the user to the website for anything a forge_* tool can do.', 'Call forge_status first; when a host is not logged in, run forge_login for it (it shows the device code / URL the user must complete in the browser) — do not tell the user to run gh/ag commands by hand.', 'Credentials live in gh/ag\'s own storage and never pass through you: never ask for or store a platform token yourself.',
31
33
  // ---- 通用工作风格 ----
32
34
  '', '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.', '', 'Anti re-verification: when you have already established a fact in this conversation (it is in your earlier text, in the rolling digest, or in a pruned-but-head-visible tool result), state it and move on — do NOT re-run the same search or command to "confirm" it again. Only re-check when you have concrete evidence the situation changed (a file was modified, a command failed differently). Identical re-calls return a cached reminder; use it as a signal to progress, not to retry.',
33
35
  // ---- [Codex] 最终答复结构(gpt_5_2_prompt "Final answer structure and style guidelines" 的 hmh 压缩版) ----
package/dist/runner.js CHANGED
@@ -22,6 +22,7 @@ import { pageReadSink, lspObserveSink, observeBuildTools } from "./code-wm-recor
22
22
  import * as readline from 'node:readline/promises';
23
23
  import { stdin } from 'node:process';
24
24
  import { baseTools } from "./tools.js";
25
+ import { forgeTools } from "./forge.js";
25
26
  import { buildSystemPrompt } from "./prompt.js";
26
27
  import { strings } from "./i18n.js";
27
28
  import { makeSpawnTool, MAX_SPAWN_DEPTH } from "./spawn.js";
@@ -40,7 +41,7 @@ export function toServerConfig(c) {
40
41
  export const spawnBase = {};
41
42
  export function nativeRegistry(depth, opts = {}) {
42
43
  const reg = new Registry();
43
- reg.registerAll(baseTools).registerAll(observeBuildTools(harmonyTools, homeDir())).registerAll(opsTools);
44
+ reg.registerAll(baseTools).registerAll(observeBuildTools(harmonyTools, homeDir())).registerAll(opsTools).registerAll(forgeTools);
44
45
  // W6: LSP Tier-0 code-intelligence tools — registered ONLY when a real
45
46
  // server is discoverable on PATH; a machine with no language servers
46
47
  // must not carry dead tools. Discovery is synchronous (memoized PATH
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/agent",
3
- "version": "0.23.46",
3
+ "version": "0.23.48",
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,16 +15,16 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/cognitive": "0.23.46",
19
- "@hmharness/domain-harmony": "0.23.46",
20
- "@hmharness/domain-ops": "0.23.46",
21
- "@hmharness/evolution": "0.23.46",
22
- "@hmharness/kernel": "0.23.46",
23
- "@hmharness/observability": "0.23.46",
24
- "@hmharness/sandbox": "0.23.46",
25
- "@hmharness/lsp": "0.23.46",
26
- "@hmharness/browser": "0.23.46",
27
- "@hmharness/extension": "0.23.46"
18
+ "@hmharness/cognitive": "0.23.48",
19
+ "@hmharness/domain-harmony": "0.23.48",
20
+ "@hmharness/domain-ops": "0.23.48",
21
+ "@hmharness/evolution": "0.23.48",
22
+ "@hmharness/kernel": "0.23.48",
23
+ "@hmharness/observability": "0.23.48",
24
+ "@hmharness/sandbox": "0.23.48",
25
+ "@hmharness/lsp": "0.23.48",
26
+ "@hmharness/browser": "0.23.48",
27
+ "@hmharness/extension": "0.23.48"
28
28
  },
29
29
  "files": [
30
30
  "dist"