harnery 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/README.md +10 -0
- package/bin/agent-coord.mjs +3 -0
- package/bin/agent-hook.mjs +3 -0
- package/bin/harn.mjs +3 -0
- package/bin/run.mjs +43 -0
- package/dist/commander.d.ts +18 -6
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +74 -2
- package/dist/commands/agents.d.ts +25 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -27
- package/dist/commands/autoupdate.d.ts +12 -0
- package/dist/commands/autoupdate.d.ts.map +1 -0
- package/dist/commands/autoupdate.js +190 -0
- package/dist/commands/computer.d.ts +45 -0
- package/dist/commands/computer.d.ts.map +1 -0
- package/dist/commands/computer.js +509 -0
- package/dist/commands/docs.js +3 -1
- package/dist/commands/fetch.js +11 -1
- package/dist/commands/grep.d.ts +72 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +200 -47
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -9
- package/dist/commands/rm.js +2 -2
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +2 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -3
- package/dist/core/agents/coord-bin.d.ts +3 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -1
- package/dist/core/agents/coord-bin.js +6 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +6 -2
- package/dist/core/agents/render/prompt-context.d.ts +55 -7
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +122 -48
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/state/proc-start.d.ts +3 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +26 -4
- package/dist/core/autoupdate/engine.d.ts +97 -0
- package/dist/core/autoupdate/engine.d.ts.map +1 -0
- package/dist/core/autoupdate/engine.js +548 -0
- package/dist/core/autoupdate/fingerprint.d.ts +16 -0
- package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
- package/dist/core/autoupdate/fingerprint.js +73 -0
- package/dist/core/autoupdate/git.d.ts +30 -0
- package/dist/core/autoupdate/git.d.ts.map +1 -0
- package/dist/core/autoupdate/git.js +82 -0
- package/dist/core/autoupdate/index.d.ts +6 -0
- package/dist/core/autoupdate/index.d.ts.map +1 -0
- package/dist/core/autoupdate/index.js +4 -0
- package/dist/core/autoupdate/state.d.ts +59 -0
- package/dist/core/autoupdate/state.d.ts.map +1 -0
- package/dist/core/autoupdate/state.js +214 -0
- package/dist/core/autoupdate/types.d.ts +113 -0
- package/dist/core/autoupdate/types.d.ts.map +1 -0
- package/dist/core/autoupdate/types.js +35 -0
- package/dist/core/config.d.ts +53 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +114 -1
- package/dist/core/events/v3/bootstrap.js +1 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +1 -0
- package/dist/core/hooks/adapter/wiring.d.ts +12 -2
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +38 -8
- package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
- package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
- package/dist/core/hooks/autoupdate-cue.js +53 -0
- package/dist/core/hooks/cli.js +52 -4
- package/dist/core/hooks/effects/index.js +4 -4
- package/dist/core/hooks/resolve/coord-root.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/semantic/contract.d.ts +3 -3
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +4 -2
- package/dist/core/storage/modes.d.ts +1 -1
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +5 -1
- package/dist/core/workflow/durable-record.d.ts +2 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +13 -1
- package/dist/core/workflow/engine.js +4 -0
- package/dist/core/workflow/spawn-claude.d.ts +7 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +16 -4
- package/dist/core/workflow/specialists.d.ts.map +1 -1
- package/dist/core/workflow/specialists.js +1 -0
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -9
- package/dist/lib/computer/cua.d.ts +50 -0
- package/dist/lib/computer/cua.d.ts.map +1 -0
- package/dist/lib/computer/cua.js +474 -0
- package/dist/lib/computer/host.d.ts +19 -0
- package/dist/lib/computer/host.d.ts.map +1 -0
- package/dist/lib/computer/host.js +72 -0
- package/dist/lib/computer/lease.d.ts +13 -0
- package/dist/lib/computer/lease.d.ts.map +1 -0
- package/dist/lib/computer/lease.js +269 -0
- package/dist/lib/computer/types.d.ts +163 -0
- package/dist/lib/computer/types.d.ts.map +1 -0
- package/dist/lib/computer/types.js +10 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
- package/dist/lib/docs-metadata-v2.js +6 -5
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +1 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +5 -4
- package/dist/lib/headless/index.d.ts +33 -0
- package/dist/lib/headless/index.d.ts.map +1 -1
- package/dist/lib/headless/index.js +38 -2
- package/dist/lib/http/client.d.ts +2 -9
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +30 -9
- package/dist/lib/instructions/apply.d.ts +9 -0
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +92 -14
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
- package/dist/lib/instructions/git-hooks.js +8 -4
- package/dist/lib/instructions/templates.d.ts +11 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +81 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +89 -1
- package/src/commander.ts +98 -8
- package/src/commands/agents.ts +100 -32
- package/src/commands/autoupdate.ts +250 -0
- package/src/commands/computer.ts +682 -0
- package/src/commands/docs.ts +3 -1
- package/src/commands/fetch.ts +11 -1
- package/src/commands/grep.ts +274 -59
- package/src/commands/init.ts +17 -7
- package/src/commands/rm.ts +2 -2
- package/src/commands/web.ts +2 -0
- package/src/core/agents/canonical-emit.ts +2 -3
- package/src/core/agents/coord-bin.ts +30 -0
- package/src/core/agents/coord-client.ts +6 -2
- package/src/core/agents/render/prompt-context.ts +152 -53
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/state/proc-start.ts +29 -5
- package/src/core/autoupdate/engine.ts +691 -0
- package/src/core/autoupdate/fingerprint.ts +89 -0
- package/src/core/autoupdate/git.ts +120 -0
- package/src/core/autoupdate/index.ts +29 -0
- package/src/core/autoupdate/state.ts +269 -0
- package/src/core/autoupdate/types.ts +135 -0
- package/src/core/config.ts +142 -1
- package/src/core/events/v3/bootstrap.ts +1 -0
- package/src/core/events/v3/runtime-build.ts +1 -0
- package/src/core/hooks/adapter/wiring.ts +51 -6
- package/src/core/hooks/autoupdate-cue.ts +72 -0
- package/src/core/hooks/cli.ts +54 -1
- package/src/core/hooks/effects/index.ts +4 -4
- package/src/core/hooks/resolve/coord-root.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/semantic/service.ts +6 -2
- package/src/core/storage/modes.ts +7 -1
- package/src/core/workflow/durable-record.ts +15 -1
- package/src/core/workflow/engine.ts +3 -0
- package/src/core/workflow/spawn-claude.ts +16 -4
- package/src/core/workflow/specialists.ts +1 -0
- package/src/core/workflow/types.ts +10 -0
- package/src/core/workflow/workspaces/leases.ts +1 -9
- package/src/lib/computer/cua.ts +572 -0
- package/src/lib/computer/host.ts +87 -0
- package/src/lib/computer/lease.ts +299 -0
- package/src/lib/computer/types.ts +177 -0
- package/src/lib/docs-metadata-v2.ts +24 -5
- package/src/lib/exec.ts +1 -0
- package/src/lib/guarded-remove.ts +5 -3
- package/src/lib/headless/index.ts +66 -2
- package/src/lib/http/client.ts +33 -19
- package/src/lib/instructions/apply.ts +100 -12
- package/src/lib/instructions/git-hooks.ts +8 -4
- package/src/lib/instructions/templates.ts +95 -1
package/src/commands/fetch.ts
CHANGED
|
@@ -90,6 +90,17 @@ async function runFetch(
|
|
|
90
90
|
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Caller headers only: the User-Agent above is ours, not the caller's.
|
|
94
|
+
if (context?.headerPolicy) {
|
|
95
|
+
const host = new URL(url).hostname;
|
|
96
|
+
for (const h of opts.header ?? []) {
|
|
97
|
+
const idx = h.indexOf(":");
|
|
98
|
+
const name = h.slice(0, idx).trim();
|
|
99
|
+
const verdict = context.headerPolicy(host, name, h.slice(idx + 1).trim());
|
|
100
|
+
if (!verdict.allow) throw new Error(verdict.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
const jar =
|
|
94
105
|
opts.cookies !== false
|
|
95
106
|
? new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-fetch" })
|
|
@@ -111,7 +122,6 @@ async function runFetch(
|
|
|
111
122
|
signal: ac.signal,
|
|
112
123
|
extraHeaders: context?.extraHeaders,
|
|
113
124
|
extraCookies: context?.extraCookies,
|
|
114
|
-
clientRedirects: context?.clientRedirects?.() || undefined,
|
|
115
125
|
responseType: "bytes",
|
|
116
126
|
});
|
|
117
127
|
} finally {
|
package/src/commands/grep.ts
CHANGED
|
@@ -35,8 +35,29 @@ import { resolveSearchEngine, type SearchEngine } from "../lib/tools/ripgrep.ts"
|
|
|
35
35
|
* envelope `{ pattern, mode, engine, and_patterns, without_patterns, repos,
|
|
36
36
|
* total_matches, total_files, truncated, elapsed_ms }`. Matches are sorted
|
|
37
37
|
* (file, then line) for stable output across runs and engines.
|
|
38
|
+
*
|
|
39
|
+
* Output and paging: a search shows every result row unless `--limit <n>`
|
|
40
|
+
* sets a page size, and clips each printed line to DEFAULT_MAX_COLUMNS
|
|
41
|
+
* characters (`--max-columns 0` turns clipping off). 500 columns keeps any
|
|
42
|
+
* hand-written source line and long prose lines while cutting minified or
|
|
43
|
+
* generated ones; on the v2 tune split a 300-column clip lost a claim that
|
|
44
|
+
* 500 kept, at about 8% more tokens than 300. There is
|
|
45
|
+
* no default row bound: on the context bench's v2 tune split, every default
|
|
46
|
+
* from 50 to 300 rows lost answer evidence that the unbounded search found
|
|
47
|
+
* (plan 2026-10-31_context-round-trips, Track D). When rows are left out, a count pass
|
|
48
|
+
* (the same search with `-c`, which prints one short row per file) supplies
|
|
49
|
+
* the totals, and the last line states how many matches and files were left
|
|
50
|
+
* out and the `--page <n>` that continues. Pages are cut from one global order
|
|
51
|
+
* (repo order, then file path, then line), so `--page 2` starts exactly where
|
|
52
|
+
* page 1 stopped, whichever engine ran. A content page is fetched by searching
|
|
53
|
+
* only the files that hold its rows.
|
|
38
54
|
*/
|
|
39
55
|
|
|
56
|
+
/** Characters kept per printed line unless --max-columns says otherwise. */
|
|
57
|
+
export const DEFAULT_MAX_COLUMNS = 500;
|
|
58
|
+
/** Explicit file arguments per engine call when fetching a content page. */
|
|
59
|
+
const PAGE_FILE_BATCH = 200;
|
|
60
|
+
|
|
40
61
|
const DEFAULT_EXCLUDE_DIRS = [
|
|
41
62
|
".git",
|
|
42
63
|
".cache",
|
|
@@ -94,6 +115,11 @@ export interface GrepOpts {
|
|
|
94
115
|
beforeContext?: string;
|
|
95
116
|
maxCount?: string;
|
|
96
117
|
limit?: string;
|
|
118
|
+
/** 1-based page of `limit`-sized pages. */
|
|
119
|
+
page?: string;
|
|
120
|
+
/** Lift the default row bound (no paging). */
|
|
121
|
+
/** Characters kept per line; "0" disables clipping. */
|
|
122
|
+
maxColumns?: string;
|
|
97
123
|
include?: string[];
|
|
98
124
|
exclude?: string[];
|
|
99
125
|
and?: string[];
|
|
@@ -110,6 +136,8 @@ export interface Match {
|
|
|
110
136
|
text: string;
|
|
111
137
|
/** "match" = primary result row (counts toward totals + --limit); "context" = free -C/-A/-B row. */
|
|
112
138
|
kind: "match" | "context";
|
|
139
|
+
/** Characters removed from `text` by --max-columns clipping (absent when whole). */
|
|
140
|
+
clipped?: number;
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
export function registerGrepCommand(
|
|
@@ -150,7 +178,12 @@ export function registerGrepCommand(
|
|
|
150
178
|
"Print N lines before each match (overrides -C's before side)",
|
|
151
179
|
)
|
|
152
180
|
.option("--max-count <n>", "Stop after N matches per file")
|
|
153
|
-
.option("--limit <n>", "
|
|
181
|
+
.option("--limit <n>", "Show at most N result rows per page (default: every row)")
|
|
182
|
+
.option("--page <n>", "Show page N of --limit-sized pages (1-based; default 1; needs --limit)")
|
|
183
|
+
.option(
|
|
184
|
+
"--max-columns <n>",
|
|
185
|
+
`Clip each printed line to N characters (default ${DEFAULT_MAX_COLUMNS}; 0 = no clipping)`,
|
|
186
|
+
)
|
|
154
187
|
.option(
|
|
155
188
|
"--and <pattern>",
|
|
156
189
|
"Only show files that ALSO contain this pattern (file-level, repeatable)",
|
|
@@ -201,15 +234,32 @@ export interface GrepResult {
|
|
|
201
234
|
and_patterns: string[];
|
|
202
235
|
without_patterns: string[];
|
|
203
236
|
repos: { name: string; cwd: string; matches: Match[]; truncated: boolean }[];
|
|
237
|
+
/** Result rows shown on this page (context rows excluded). */
|
|
204
238
|
total_matches: number;
|
|
239
|
+
/** Files with at least one shown row. */
|
|
205
240
|
total_files: number;
|
|
241
|
+
/** More rows exist after this page. */
|
|
206
242
|
truncated: boolean;
|
|
243
|
+
/** Page size; null without --limit. */
|
|
244
|
+
limit: number | null;
|
|
245
|
+
page: number;
|
|
246
|
+
/** Result rows / files the whole search found, across every page. */
|
|
247
|
+
available_matches: number;
|
|
248
|
+
available_files: number;
|
|
249
|
+
/** Rows / files the search found that this page does not show. */
|
|
250
|
+
omitted_matches: number;
|
|
251
|
+
omitted_files: number;
|
|
252
|
+
/** The --page value that continues, or null on the last page. */
|
|
253
|
+
next_page: number | null;
|
|
207
254
|
elapsed_ms: number;
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
/** Validated, resolved options — built once before repo fan-out. */
|
|
211
258
|
interface NormOpts {
|
|
212
|
-
limit: number; // Infinity
|
|
259
|
+
limit: number; // page size; Infinity without --limit
|
|
260
|
+
offset: number; // rows skipped before this page
|
|
261
|
+
page: number;
|
|
262
|
+
maxColumns: number; // Infinity when clipping is off
|
|
213
263
|
maxCount: number | undefined;
|
|
214
264
|
before: number;
|
|
215
265
|
after: number;
|
|
@@ -257,6 +307,11 @@ function normalizeLangs(lang: string | string[] | undefined): string[] | undefin
|
|
|
257
307
|
/** Validate numerics, resolve context sides, expand langs, enforce the compatibility matrix. */
|
|
258
308
|
function normalizeOpts(opts: GrepOpts): NormOpts {
|
|
259
309
|
const limit = opts.limit ? parseIntOpt("--limit", opts.limit, 1) : Number.POSITIVE_INFINITY;
|
|
310
|
+
const page = opts.page ? parseIntOpt("--page", opts.page, 1) : 1;
|
|
311
|
+
const maxColumnsRaw =
|
|
312
|
+
opts.maxColumns !== undefined
|
|
313
|
+
? parseIntOpt("--max-columns", opts.maxColumns, 0)
|
|
314
|
+
: DEFAULT_MAX_COLUMNS;
|
|
260
315
|
const maxCount = opts.maxCount ? parseIntOpt("--max-count", opts.maxCount, 1) : undefined;
|
|
261
316
|
const c = opts.context !== undefined ? parseIntOpt("-C/--context", opts.context, 0) : 0;
|
|
262
317
|
const before =
|
|
@@ -303,15 +358,20 @@ function normalizeOpts(opts: GrepOpts): NormOpts {
|
|
|
303
358
|
[opts.filesOnly, "-l/--files-only"],
|
|
304
359
|
[opts.count, "-c/--count"],
|
|
305
360
|
[opts.limit, "--limit"],
|
|
361
|
+
[opts.page, "--page"],
|
|
306
362
|
];
|
|
307
363
|
for (const [set, flag] of rejected) {
|
|
308
364
|
if (set) throw new Error(`-q/--quiet does not combine with ${flag}`);
|
|
309
365
|
}
|
|
310
366
|
}
|
|
367
|
+
if (opts.page && !opts.limit) throw new Error("--page needs --limit (the page size)");
|
|
311
368
|
|
|
312
369
|
return {
|
|
313
370
|
// -q only needs existence: one accepted primary row settles the exit code.
|
|
314
371
|
limit: opts.quiet ? 1 : limit,
|
|
372
|
+
offset: opts.quiet || !Number.isFinite(limit) ? 0 : (page - 1) * limit,
|
|
373
|
+
page,
|
|
374
|
+
maxColumns: maxColumnsRaw === 0 ? Number.POSITIVE_INFINITY : maxColumnsRaw,
|
|
315
375
|
maxCount,
|
|
316
376
|
before,
|
|
317
377
|
after,
|
|
@@ -339,66 +399,148 @@ export async function runGrep(
|
|
|
339
399
|
// ride the same --no-default-excludes gate as the built-in list.
|
|
340
400
|
const hostExcludeDirs = opts.noDefaultExcludes ? [] : (context?.grepExcludeDirs ?? []);
|
|
341
401
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
402
|
+
// In --all-repos mode the parent scan prunes submodule dirs — each
|
|
403
|
+
// submodule gets its own scoped scan, so descending from the parent would
|
|
404
|
+
// double-scan and double-report every submodule match. This pruning is
|
|
405
|
+
// correctness (one repo owns each match), so it applies even when
|
|
406
|
+
// --no-default-excludes is set.
|
|
407
|
+
const extraDirsFor = (repo: { name: string }): string[] => {
|
|
408
|
+
const dedupeDirs = opts.allRepos && repo.name === "parent" ? (context?.submodules ?? []) : [];
|
|
409
|
+
return [...hostExcludeDirs, ...dedupeDirs];
|
|
410
|
+
};
|
|
411
|
+
const scan = (
|
|
412
|
+
repo: { name: string; cwd: string },
|
|
413
|
+
scanPaths: string[],
|
|
414
|
+
scanOpts: GrepOpts,
|
|
415
|
+
scanNorm: NormOpts,
|
|
416
|
+
acceptLimit: number,
|
|
417
|
+
): Promise<Match[]> =>
|
|
418
|
+
runGrepInRepo(
|
|
419
|
+
pattern,
|
|
420
|
+
scanPaths,
|
|
421
|
+
scanOpts,
|
|
422
|
+
scanNorm,
|
|
423
|
+
repo.cwd,
|
|
424
|
+
repo.name,
|
|
425
|
+
acceptLimit,
|
|
426
|
+
engine,
|
|
427
|
+
rgBin,
|
|
428
|
+
extraDirsFor(repo),
|
|
429
|
+
);
|
|
430
|
+
const byPosition = (a: Match, b: Match) =>
|
|
431
|
+
a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line;
|
|
432
|
+
|
|
433
|
+
// Every path yields, per repo, the selected rows (sorted) plus what the
|
|
434
|
+
// whole search found there, so the page window can be stated exactly.
|
|
435
|
+
let selected: Match[][];
|
|
436
|
+
let available: { matches: number; files: Set<string> }[];
|
|
437
|
+
let after: boolean[]; // rows exist in this repo after the page window
|
|
438
|
+
|
|
439
|
+
if (opts.quiet) {
|
|
440
|
+
// Existence only: one accepted row settles the exit code, so the engine is
|
|
441
|
+
// stopped at the first hit and no count pass runs.
|
|
442
|
+
selected = await Promise.all(repos.map((repo) => scan(repo, paths, opts, norm, 1)));
|
|
443
|
+
available = selected.map((rows) => ({
|
|
444
|
+
matches: rows.length,
|
|
445
|
+
files: new Set(rows.map((m) => m.file)),
|
|
446
|
+
}));
|
|
447
|
+
after = selected.map(() => false);
|
|
448
|
+
} else if (!Number.isFinite(norm.limit) || opts.files || opts.filesOnly || opts.count) {
|
|
449
|
+
// No page size, or a mode whose rows are files (one short row each): collect the
|
|
450
|
+
// complete result, then cut the page from its sorted order.
|
|
451
|
+
const all = await Promise.all(
|
|
452
|
+
repos.map((repo) => scan(repo, paths, opts, norm, Number.POSITIVE_INFINITY)),
|
|
453
|
+
);
|
|
454
|
+
for (const rows of all) rows.sort(byPosition);
|
|
455
|
+
available = all.map((rows) => ({
|
|
456
|
+
matches: rows.length,
|
|
457
|
+
files: new Set(rows.map((m) => m.file)),
|
|
458
|
+
}));
|
|
459
|
+
const windows = pageWindows(
|
|
460
|
+
all.map((rows) => rows.map(() => 1)),
|
|
461
|
+
norm.offset,
|
|
462
|
+
norm.limit,
|
|
463
|
+
);
|
|
464
|
+
selected = all.map((rows, i) => rows.slice(windows[i]?.start ?? 0, windows[i]?.end ?? 0));
|
|
465
|
+
after = all.map((rows, i) => (windows[i]?.end ?? 0) < rows.length);
|
|
466
|
+
} else {
|
|
467
|
+
// Content page. The count pass reports one row per file (path + matching
|
|
468
|
+
// line count) for the whole search; the page window is computed from those
|
|
469
|
+
// counts, and only the files holding the window's rows are searched again.
|
|
470
|
+
const countOpts: GrepOpts = { ...opts, count: true };
|
|
471
|
+
const counts = await Promise.all(
|
|
472
|
+
repos.map((repo) => scan(repo, paths, countOpts, norm, Number.POSITIVE_INFINITY)),
|
|
473
|
+
);
|
|
474
|
+
for (const rows of counts) rows.sort(byPosition);
|
|
475
|
+
available = counts.map((rows) => ({
|
|
476
|
+
matches: rows.reduce((sum, r) => sum + r.line, 0),
|
|
477
|
+
files: new Set(rows.map((r) => r.file)),
|
|
478
|
+
}));
|
|
479
|
+
// Expand per-file counts into a row index space without materializing rows.
|
|
480
|
+
const windows = pageWindows(
|
|
481
|
+
counts.map((rows) => rows.map((r) => r.line)),
|
|
482
|
+
norm.offset,
|
|
483
|
+
norm.limit,
|
|
484
|
+
);
|
|
485
|
+
// The selected files already passed --and/--without, so the page fetch
|
|
486
|
+
// skips the membership scans.
|
|
487
|
+
const fetchNorm: NormOpts = { ...norm, andPatterns: [], withoutPatterns: [] };
|
|
488
|
+
selected = await Promise.all(
|
|
489
|
+
repos.map(async (repo, i) => {
|
|
490
|
+
const w = windows[i];
|
|
491
|
+
const rows = counts[i] ?? [];
|
|
492
|
+
if (!w || w.start >= w.end) return [];
|
|
493
|
+
const picks = sliceFiles(rows, w.start, w.end);
|
|
494
|
+
const fetched: Match[] = [];
|
|
495
|
+
for (let b = 0; b < picks.length; b += PAGE_FILE_BATCH) {
|
|
496
|
+
const batch = picks.slice(b, b + PAGE_FILE_BATCH);
|
|
497
|
+
fetched.push(
|
|
498
|
+
...(await scan(
|
|
499
|
+
repo,
|
|
500
|
+
batch.map((p) => p.file),
|
|
501
|
+
opts,
|
|
502
|
+
fetchNorm,
|
|
503
|
+
Number.POSITIVE_INFINITY,
|
|
504
|
+
)),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
fetched.sort(byPosition);
|
|
508
|
+
const out: Match[] = [];
|
|
509
|
+
for (const pick of picks) {
|
|
510
|
+
const inFile = fetched.filter((m) => m.file === pick.file);
|
|
511
|
+
out.push(...inFile.slice(pick.skip, pick.skip + pick.take));
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
after = counts.map((_, i) => (windows[i]?.end ?? 0) < (available[i]?.matches ?? 0));
|
|
517
|
+
}
|
|
370
518
|
|
|
371
519
|
const allRepoResults: GrepResult["repos"] = [];
|
|
372
520
|
let totalMatches = 0;
|
|
373
521
|
const filesSeen = new Set<string>();
|
|
374
|
-
let
|
|
375
|
-
let
|
|
522
|
+
let availableMatches = 0;
|
|
523
|
+
let availableFiles = 0;
|
|
376
524
|
|
|
377
525
|
for (let i = 0; i < repos.length; i++) {
|
|
378
526
|
const repo = repos[i];
|
|
379
527
|
if (!repo) continue;
|
|
380
|
-
|
|
381
|
-
collected.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
|
|
382
|
-
const take = Number.isFinite(budget)
|
|
383
|
-
? Math.min(collected.length, Math.max(0, budget))
|
|
384
|
-
: collected.length;
|
|
385
|
-
let matches = collected.slice(0, take);
|
|
386
|
-
// truncated only when an accepted primary row was actually omitted —
|
|
387
|
-
// the lookahead row (or a later-repo surplus) is the proof.
|
|
388
|
-
const repoTruncated = collected.length > take;
|
|
389
|
-
if (repoTruncated) truncated = true;
|
|
390
|
-
if (Number.isFinite(budget)) budget -= take;
|
|
528
|
+
let matches = selected[i] ?? [];
|
|
391
529
|
totalMatches += matches.length;
|
|
530
|
+
availableMatches += available[i]?.matches ?? 0;
|
|
531
|
+
availableFiles += available[i]?.files.size ?? 0;
|
|
392
532
|
for (const m of matches) filesSeen.add(`${repo.name}/${m.file}`);
|
|
393
|
-
// Context is materialized only for the SELECTED rows, after
|
|
394
|
-
//
|
|
395
|
-
//
|
|
533
|
+
// Context is materialized only for the SELECTED rows, after paging, so
|
|
534
|
+
// context rows never consume the limit and trailing context is never cut
|
|
535
|
+
// off by an engine kill.
|
|
396
536
|
if (!opts.files && !opts.filesOnly && !opts.count && (norm.before > 0 || norm.after > 0)) {
|
|
397
537
|
matches = await materializeContext(matches, repo.cwd, norm.before, norm.after);
|
|
398
538
|
}
|
|
399
|
-
|
|
539
|
+
if (Number.isFinite(norm.maxColumns)) matches = matches.map((m) => clip(m, norm.maxColumns));
|
|
540
|
+
allRepoResults.push({ name: repo.name, cwd: repo.cwd, matches, truncated: after[i] ?? false });
|
|
400
541
|
}
|
|
401
542
|
|
|
543
|
+
const truncated = allRepoResults.some((r) => r.truncated);
|
|
402
544
|
return {
|
|
403
545
|
pattern,
|
|
404
546
|
mode: opts.files ? "files" : opts.literal ? "literal" : "regex",
|
|
@@ -409,10 +551,66 @@ export async function runGrep(
|
|
|
409
551
|
total_matches: totalMatches,
|
|
410
552
|
total_files: filesSeen.size,
|
|
411
553
|
truncated,
|
|
554
|
+
limit: Number.isFinite(norm.limit) && !opts.quiet ? norm.limit : null,
|
|
555
|
+
page: opts.quiet ? 1 : norm.page,
|
|
556
|
+
available_matches: availableMatches,
|
|
557
|
+
available_files: availableFiles,
|
|
558
|
+
omitted_matches: availableMatches - totalMatches,
|
|
559
|
+
omitted_files: availableFiles - filesSeen.size,
|
|
560
|
+
next_page: truncated && !opts.quiet ? norm.page + 1 : null,
|
|
412
561
|
elapsed_ms: Date.now() - started,
|
|
413
562
|
};
|
|
414
563
|
}
|
|
415
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Cut the page window [offset, offset + limit) out of one global row order:
|
|
567
|
+
* repos in order, each repo's rows in sorted order. `weights[i][j]` is how many
|
|
568
|
+
* rows item j of repo i stands for (1 for a row, a file's count in the count
|
|
569
|
+
* pass). Returns each repo's window as row indices local to that repo.
|
|
570
|
+
*/
|
|
571
|
+
function pageWindows(
|
|
572
|
+
weights: number[][],
|
|
573
|
+
offset: number,
|
|
574
|
+
limit: number,
|
|
575
|
+
): { start: number; end: number }[] {
|
|
576
|
+
const end = offset + limit;
|
|
577
|
+
let base = 0;
|
|
578
|
+
return weights.map((items) => {
|
|
579
|
+
const size = items.reduce((sum, w) => sum + w, 0);
|
|
580
|
+
const start = Math.min(size, Math.max(0, offset - base));
|
|
581
|
+
const stop = Math.min(size, Math.max(0, end - base));
|
|
582
|
+
base += size;
|
|
583
|
+
return { start, end: stop };
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Which files (and which of their rows) a local row window covers. */
|
|
588
|
+
function sliceFiles(
|
|
589
|
+
counts: Match[],
|
|
590
|
+
start: number,
|
|
591
|
+
end: number,
|
|
592
|
+
): { file: string; skip: number; take: number }[] {
|
|
593
|
+
const picks: { file: string; skip: number; take: number }[] = [];
|
|
594
|
+
let pos = 0;
|
|
595
|
+
for (const row of counts) {
|
|
596
|
+
const fileStart = pos;
|
|
597
|
+
const fileEnd = pos + row.line;
|
|
598
|
+
pos = fileEnd;
|
|
599
|
+
if (fileEnd <= start) continue;
|
|
600
|
+
if (fileStart >= end) break;
|
|
601
|
+
const skip = Math.max(0, start - fileStart);
|
|
602
|
+
const take = Math.min(fileEnd, end) - fileStart - skip;
|
|
603
|
+
if (take > 0) picks.push({ file: row.file, skip, take });
|
|
604
|
+
}
|
|
605
|
+
return picks;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Clip a row's text to `max` characters, recording how many were removed. */
|
|
609
|
+
function clip(m: Match, max: number): Match {
|
|
610
|
+
if (m.text.length <= max) return m;
|
|
611
|
+
return { ...m, text: m.text.slice(0, max), clipped: m.text.length - max };
|
|
612
|
+
}
|
|
613
|
+
|
|
416
614
|
function resolveRepos(
|
|
417
615
|
opts: GrepOpts,
|
|
418
616
|
context: HarneryProgramContext | undefined,
|
|
@@ -967,7 +1165,8 @@ function normalizeFile(file: string): string {
|
|
|
967
1165
|
return file.startsWith("./") ? file.slice(2) : file;
|
|
968
1166
|
}
|
|
969
1167
|
|
|
970
|
-
|
|
1168
|
+
/** Exported for tests (not part of the package exports map). */
|
|
1169
|
+
export function renderResult(r: GrepResult, opts: GrepOpts): string {
|
|
971
1170
|
const lines: string[] = [];
|
|
972
1171
|
const showRepoHeader = r.repos.length > 1;
|
|
973
1172
|
for (const repo of r.repos) {
|
|
@@ -988,25 +1187,41 @@ function renderResult(r: GrepResult, opts: GrepOpts): string {
|
|
|
988
1187
|
lines.push(m.file);
|
|
989
1188
|
} else if (opts.count) {
|
|
990
1189
|
lines.push(`${m.file}:${m.line}`);
|
|
991
|
-
} else if (m.kind === "context") {
|
|
992
|
-
lines.push(`${m.file}-${m.line}-${m.text}`);
|
|
993
1190
|
} else {
|
|
994
|
-
|
|
1191
|
+
const sep = m.kind === "context" ? "-" : ":";
|
|
1192
|
+
const more = m.clipped ? ` …[+${m.clipped} chars]` : "";
|
|
1193
|
+
lines.push(`${m.file}${sep}${m.line}${sep}${m.text}${more}`);
|
|
995
1194
|
}
|
|
996
1195
|
prev = m;
|
|
997
1196
|
}
|
|
998
|
-
if (repo.truncated) {
|
|
999
|
-
lines.push(" (truncated; raise --limit)");
|
|
1000
|
-
}
|
|
1001
1197
|
}
|
|
1002
|
-
|
|
1198
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1199
|
+
const matchWord = (n: number) => `${n} match${n === 1 ? "" : "es"}`;
|
|
1200
|
+
const fileRows = Boolean(opts.files || opts.filesOnly || opts.count);
|
|
1201
|
+
if (r.total_matches === 0 && r.available_matches === 0) {
|
|
1003
1202
|
const modeTag = r.mode === "literal" ? " (literal)" : r.mode === "files" ? " (files)" : "";
|
|
1004
1203
|
lines.push(`(no matches for /${r.pattern}/${modeTag})`);
|
|
1005
|
-
} else if (r.
|
|
1204
|
+
} else if (r.omitted_matches > 0) {
|
|
1205
|
+
// Something was left out: state the window, what was left out, and how to
|
|
1206
|
+
// continue. Never printed when the page shows everything.
|
|
1207
|
+
const first = r.limit === null ? 1 : (r.page - 1) * r.limit + 1;
|
|
1208
|
+
const last = first + r.total_matches - 1;
|
|
1209
|
+
const shown =
|
|
1210
|
+
r.total_matches === 0
|
|
1211
|
+
? `Page ${r.page} is past the end`
|
|
1212
|
+
: fileRows
|
|
1213
|
+
? `Showing ${first}-${last} of ${plural(r.available_matches, "file")}`
|
|
1214
|
+
: `Showing matches ${first}-${last} of ${r.available_matches} (in ${plural(r.available_files, "file")})`;
|
|
1215
|
+
const left = fileRows
|
|
1216
|
+
? `${plural(r.omitted_matches, "file")} left out`
|
|
1217
|
+
: `${matchWord(r.omitted_matches)} in ${plural(r.omitted_files, "other file")} left out`;
|
|
1218
|
+
const next = r.next_page !== null ? `. Next: --page ${r.next_page}` : "";
|
|
1219
|
+
lines.push("");
|
|
1220
|
+
lines.push(`${shown}; ${left}${next}. (${r.elapsed_ms}ms, ${r.engine})`);
|
|
1221
|
+
} else if (r.repos.length > 1) {
|
|
1006
1222
|
lines.push("");
|
|
1007
|
-
const summary = `${r.total_matches}
|
|
1008
|
-
|
|
1009
|
-
lines.push(`${summary}${tail} (${r.elapsed_ms}ms, ${r.engine})`);
|
|
1223
|
+
const summary = `${matchWord(r.total_matches)} across ${plural(r.total_files, "file")}`;
|
|
1224
|
+
lines.push(`${summary} (${r.elapsed_ms}ms, ${r.engine})`);
|
|
1010
1225
|
}
|
|
1011
1226
|
return lines.join("\n");
|
|
1012
1227
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
codexWindowsBridgeConfig,
|
|
32
32
|
DEFAULT_BIN_NAME,
|
|
33
33
|
pinnedBinName,
|
|
34
|
+
portableHookCommands,
|
|
34
35
|
stripJsonComments,
|
|
35
36
|
} from "../core/config.ts";
|
|
36
37
|
import { canonicalJsonV3 } from "../core/events/v3/canonical.ts";
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
isAgentHookCommand,
|
|
62
63
|
makeEntry,
|
|
63
64
|
orderHandlerKeys,
|
|
65
|
+
projectWiringOptions,
|
|
64
66
|
type SettingsFile,
|
|
65
67
|
summarizeAdapterWiring,
|
|
66
68
|
type WiringOptions,
|
|
@@ -133,7 +135,8 @@ function checkAdapterHooks(
|
|
|
133
135
|
bin: string,
|
|
134
136
|
): { drift: boolean; error: boolean; issues: string[] } {
|
|
135
137
|
const spec = ADAPTER_SPECS[adapter];
|
|
136
|
-
const
|
|
138
|
+
const options = projectWiringOptions(projectRoot);
|
|
139
|
+
const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT, options);
|
|
137
140
|
if (spec.installMode === "opencode-plugin") {
|
|
138
141
|
// No hooks map to diff: the plugin directory is the wiring.
|
|
139
142
|
const plugin = checkOpenCodePlugin(projectRoot, { binName: bin, agentHook });
|
|
@@ -159,7 +162,7 @@ function checkAdapterHooks(
|
|
|
159
162
|
const hookDiff = diffWiring(settings, spec, {
|
|
160
163
|
agentHookPath: agentHook,
|
|
161
164
|
adapter,
|
|
162
|
-
|
|
165
|
+
...options,
|
|
163
166
|
});
|
|
164
167
|
drift =
|
|
165
168
|
hookDiff.missing.length > 0 ||
|
|
@@ -444,7 +447,8 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
444
447
|
for (const adapter of adapters) {
|
|
445
448
|
const spec = ADAPTER_SPECS[adapter];
|
|
446
449
|
const settingsPath = resolve(projectRoot, spec.settingsFile);
|
|
447
|
-
const
|
|
450
|
+
const portableCommands = portableHookCommands(projectRoot);
|
|
451
|
+
const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT, { portableCommands });
|
|
448
452
|
|
|
449
453
|
if (spec.installMode === "opencode-plugin") {
|
|
450
454
|
// OpenCode has no hooks map. Install the plugin directory instead; the
|
|
@@ -475,14 +479,16 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
475
479
|
}
|
|
476
480
|
const bridge = codexWindowsBridgeConfig(projectRoot);
|
|
477
481
|
if (bridge.error && spec.windowsBridgeHooks) {
|
|
478
|
-
actions.push(
|
|
482
|
+
actions.push(
|
|
483
|
+
`⚠ ${bridge.error}; writing ${adapter} hooks without the Windows bridge command`,
|
|
484
|
+
);
|
|
479
485
|
}
|
|
480
486
|
const { wired, already, removed, upgraded } = wireHooks(
|
|
481
487
|
settings,
|
|
482
488
|
spec,
|
|
483
489
|
agentHook,
|
|
484
490
|
adapter,
|
|
485
|
-
{ windowsBridgeEntryPoint: bridge.entryPoint },
|
|
491
|
+
{ windowsBridgeEntryPoint: bridge.entryPoint, portableCommands },
|
|
486
492
|
);
|
|
487
493
|
|
|
488
494
|
if (wired === 0 && removed === 0 && upgraded === 0) {
|
|
@@ -544,6 +550,7 @@ export function registerInitCommand(program: Command, emit: EmitContext, binName
|
|
|
544
550
|
|
|
545
551
|
function gitBuild(root: string): string {
|
|
546
552
|
const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
553
|
+
windowsHide: true,
|
|
547
554
|
encoding: "utf8",
|
|
548
555
|
timeout: 5000,
|
|
549
556
|
});
|
|
@@ -624,7 +631,7 @@ export function wireHooks(
|
|
|
624
631
|
}
|
|
625
632
|
}
|
|
626
633
|
for (const { settingsKey, subcommand } of spec.events) {
|
|
627
|
-
const command = hookCommand(spec, agentHookPath, subcommand, adapter);
|
|
634
|
+
const command = hookCommand(spec, agentHookPath, subcommand, adapter, options);
|
|
628
635
|
const timeout = hookTimeoutFor(spec, subcommand);
|
|
629
636
|
const commandWindows = expectedCommandWindows(
|
|
630
637
|
spec,
|
|
@@ -974,7 +981,10 @@ function renderInstructionsOnly(
|
|
|
974
981
|
}
|
|
975
982
|
|
|
976
983
|
function gitTopLevel(): string | null {
|
|
977
|
-
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
984
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
985
|
+
encoding: "utf8",
|
|
986
|
+
windowsHide: true,
|
|
987
|
+
});
|
|
978
988
|
const out = r.status === 0 ? r.stdout.trim() : "";
|
|
979
989
|
return out || null;
|
|
980
990
|
}
|
package/src/commands/rm.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function registerRmCommand(
|
|
|
23
23
|
.description("Preview guarded removal of untracked files; --yes applies permanent deletion")
|
|
24
24
|
.requiredOption(
|
|
25
25
|
"--root <directory>",
|
|
26
|
-
"Allowed directory; each target must be strictly inside it",
|
|
26
|
+
"Allowed directory; relative targets resolve under it, and each target must be strictly inside it",
|
|
27
27
|
)
|
|
28
28
|
.option("-r, --recursive", "Permit directory removal after inspecting its complete contents")
|
|
29
29
|
.option(
|
|
@@ -37,7 +37,7 @@ export function registerRmCommand(
|
|
|
37
37
|
)
|
|
38
38
|
.option(
|
|
39
39
|
"--allow-protected <path>",
|
|
40
|
-
"Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; repeat per path",
|
|
40
|
+
"Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; relative paths resolve under --root; repeat per path",
|
|
41
41
|
(value: string, previous: string[] = []) => [...previous, value],
|
|
42
42
|
)
|
|
43
43
|
.option("--yes", "Permanently remove validated targets")
|
package/src/commands/web.ts
CHANGED
|
@@ -267,6 +267,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
267
267
|
if (heapMb > 0) emit.log(`heap ceiling · ${heapMb} MB`, "info");
|
|
268
268
|
|
|
269
269
|
const child = spawn(webRunner(), ["run", mode], {
|
|
270
|
+
windowsHide: true,
|
|
270
271
|
cwd: root,
|
|
271
272
|
env: {
|
|
272
273
|
...process.env,
|
|
@@ -360,6 +361,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
360
361
|
if (heapMb > 0) emit.log(`heap ceiling · ${heapMb} MB`, "info");
|
|
361
362
|
|
|
362
363
|
const child = spawn(webRunner(), ["run", "start"], {
|
|
364
|
+
windowsHide: true,
|
|
363
365
|
cwd: root,
|
|
364
366
|
env: {
|
|
365
367
|
...process.env,
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
* seconds and record the observation before the parent command exits.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
13
|
import { type EventAdapterIdV3, eventAdapterIdV3FromPlatform } from "../events/v3/adapter-id.ts";
|
|
15
14
|
import type { LiveCoordinationObservationV3 } from "../events/v3/live-observation.ts";
|
|
16
|
-
import { coordBinPath } from "./coord-bin.ts";
|
|
15
|
+
import { coordBinPath, spawnCoordBinSync } from "./coord-bin.ts";
|
|
17
16
|
import { resolveCoordRoot } from "./coord-client.ts";
|
|
18
17
|
import type { Heartbeat } from "./state/heartbeat-reader.ts";
|
|
19
18
|
|
|
@@ -64,7 +63,7 @@ export function emitEventV3(input: EventV3EmitInput): boolean {
|
|
|
64
63
|
];
|
|
65
64
|
// Pin cwd + HARNERY_COORD_ROOT_OVERRIDE so the child writes to the SAME
|
|
66
65
|
// stream we resolved, regardless of where the caller's shell is cd'd.
|
|
67
|
-
const result =
|
|
66
|
+
const result = spawnCoordBinSync(binary, args, {
|
|
68
67
|
encoding: "utf8",
|
|
69
68
|
stdio: ["pipe", "ignore", "pipe"],
|
|
70
69
|
input: JSON.stringify(input.observation),
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* still resolves.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import {
|
|
25
|
+
type SpawnSyncOptions,
|
|
26
|
+
type SpawnSyncOptionsWithStringEncoding,
|
|
27
|
+
type SpawnSyncReturns,
|
|
28
|
+
spawnSync,
|
|
29
|
+
} from "node:child_process";
|
|
24
30
|
import { existsSync } from "node:fs";
|
|
25
31
|
import { dirname, join, resolve } from "node:path";
|
|
26
32
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,6 +34,30 @@ import { coordEnv } from "../../lib/env.ts";
|
|
|
28
34
|
|
|
29
35
|
export type CoordBinName = "agent-coord" | "agent-hook" | "harn";
|
|
30
36
|
|
|
37
|
+
export function spawnCoordBinSync(
|
|
38
|
+
binary: string,
|
|
39
|
+
args: readonly string[],
|
|
40
|
+
options: SpawnSyncOptionsWithStringEncoding,
|
|
41
|
+
): SpawnSyncReturns<string>;
|
|
42
|
+
export function spawnCoordBinSync(
|
|
43
|
+
binary: string,
|
|
44
|
+
args: readonly string[],
|
|
45
|
+
options?: SpawnSyncOptions,
|
|
46
|
+
): SpawnSyncReturns<string | Buffer>;
|
|
47
|
+
/** Execute the native launcher on Windows, preserving stdin, argv and blocking exit codes. */
|
|
48
|
+
export function spawnCoordBinSync(
|
|
49
|
+
binary: string,
|
|
50
|
+
args: readonly string[],
|
|
51
|
+
options: SpawnSyncOptions = {},
|
|
52
|
+
): SpawnSyncReturns<string | Buffer> {
|
|
53
|
+
const native = process.platform === "win32";
|
|
54
|
+
return spawnSync(
|
|
55
|
+
native ? process.execPath : binary,
|
|
56
|
+
native ? [`${binary}.mjs`, ...args] : [...args],
|
|
57
|
+
{ windowsHide: true, ...options },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
/**
|
|
32
62
|
* Absolute path to one of harnery's bundled binaries, or null when no candidate
|
|
33
63
|
* exists on disk. Callers must handle null — a missing helper is a real state
|