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.
Files changed (187) hide show
  1. package/README.md +24 -0
  2. package/bin/README.md +10 -0
  3. package/bin/agent-coord.mjs +3 -0
  4. package/bin/agent-hook.mjs +3 -0
  5. package/bin/harn.mjs +3 -0
  6. package/bin/run.mjs +43 -0
  7. package/dist/commander.d.ts +18 -6
  8. package/dist/commander.d.ts.map +1 -1
  9. package/dist/commander.js +74 -2
  10. package/dist/commands/agents.d.ts +25 -0
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/agents.js +70 -27
  13. package/dist/commands/autoupdate.d.ts +12 -0
  14. package/dist/commands/autoupdate.d.ts.map +1 -0
  15. package/dist/commands/autoupdate.js +190 -0
  16. package/dist/commands/computer.d.ts +45 -0
  17. package/dist/commands/computer.d.ts.map +1 -0
  18. package/dist/commands/computer.js +509 -0
  19. package/dist/commands/docs.js +3 -1
  20. package/dist/commands/fetch.js +11 -1
  21. package/dist/commands/grep.d.ts +72 -0
  22. package/dist/commands/grep.d.ts.map +1 -1
  23. package/dist/commands/grep.js +200 -47
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +15 -9
  26. package/dist/commands/rm.js +2 -2
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +2 -0
  29. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  30. package/dist/core/agents/canonical-emit.js +2 -3
  31. package/dist/core/agents/coord-bin.d.ts +3 -0
  32. package/dist/core/agents/coord-bin.d.ts.map +1 -1
  33. package/dist/core/agents/coord-bin.js +6 -0
  34. package/dist/core/agents/coord-client.d.ts.map +1 -1
  35. package/dist/core/agents/coord-client.js +6 -2
  36. package/dist/core/agents/render/prompt-context.d.ts +55 -7
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +122 -48
  39. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  40. package/dist/core/agents/rules/claim-conflict.js +36 -16
  41. package/dist/core/agents/rules/stop-hook.d.ts +6 -0
  42. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  43. package/dist/core/agents/rules/stop-hook.js +20 -1
  44. package/dist/core/agents/state/proc-start.d.ts +3 -1
  45. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  46. package/dist/core/agents/state/proc-start.js +26 -4
  47. package/dist/core/autoupdate/engine.d.ts +97 -0
  48. package/dist/core/autoupdate/engine.d.ts.map +1 -0
  49. package/dist/core/autoupdate/engine.js +548 -0
  50. package/dist/core/autoupdate/fingerprint.d.ts +16 -0
  51. package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
  52. package/dist/core/autoupdate/fingerprint.js +73 -0
  53. package/dist/core/autoupdate/git.d.ts +30 -0
  54. package/dist/core/autoupdate/git.d.ts.map +1 -0
  55. package/dist/core/autoupdate/git.js +82 -0
  56. package/dist/core/autoupdate/index.d.ts +6 -0
  57. package/dist/core/autoupdate/index.d.ts.map +1 -0
  58. package/dist/core/autoupdate/index.js +4 -0
  59. package/dist/core/autoupdate/state.d.ts +59 -0
  60. package/dist/core/autoupdate/state.d.ts.map +1 -0
  61. package/dist/core/autoupdate/state.js +214 -0
  62. package/dist/core/autoupdate/types.d.ts +113 -0
  63. package/dist/core/autoupdate/types.d.ts.map +1 -0
  64. package/dist/core/autoupdate/types.js +35 -0
  65. package/dist/core/config.d.ts +53 -1
  66. package/dist/core/config.d.ts.map +1 -1
  67. package/dist/core/config.js +114 -1
  68. package/dist/core/events/v3/bootstrap.js +1 -0
  69. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  70. package/dist/core/events/v3/runtime-build.js +1 -0
  71. package/dist/core/hooks/adapter/wiring.d.ts +12 -2
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +38 -8
  74. package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
  75. package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
  76. package/dist/core/hooks/autoupdate-cue.js +53 -0
  77. package/dist/core/hooks/cli.js +52 -4
  78. package/dist/core/hooks/effects/index.js +4 -4
  79. package/dist/core/hooks/resolve/coord-root.js +1 -0
  80. package/dist/core/hooks/resolve/transcript.d.ts +12 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  82. package/dist/core/hooks/resolve/transcript.js +54 -0
  83. package/dist/core/semantic/contract.d.ts +3 -3
  84. package/dist/core/semantic/service.d.ts.map +1 -1
  85. package/dist/core/semantic/service.js +4 -2
  86. package/dist/core/storage/modes.d.ts +1 -1
  87. package/dist/core/storage/modes.d.ts.map +1 -1
  88. package/dist/core/storage/modes.js +5 -1
  89. package/dist/core/workflow/durable-record.d.ts +2 -0
  90. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  91. package/dist/core/workflow/durable-record.js +13 -1
  92. package/dist/core/workflow/engine.js +4 -0
  93. package/dist/core/workflow/spawn-claude.d.ts +7 -0
  94. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  95. package/dist/core/workflow/spawn-claude.js +16 -4
  96. package/dist/core/workflow/specialists.d.ts.map +1 -1
  97. package/dist/core/workflow/specialists.js +1 -0
  98. package/dist/core/workflow/types.d.ts +10 -0
  99. package/dist/core/workflow/types.d.ts.map +1 -1
  100. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  101. package/dist/core/workflow/workspaces/leases.js +1 -9
  102. package/dist/lib/computer/cua.d.ts +50 -0
  103. package/dist/lib/computer/cua.d.ts.map +1 -0
  104. package/dist/lib/computer/cua.js +474 -0
  105. package/dist/lib/computer/host.d.ts +19 -0
  106. package/dist/lib/computer/host.d.ts.map +1 -0
  107. package/dist/lib/computer/host.js +72 -0
  108. package/dist/lib/computer/lease.d.ts +13 -0
  109. package/dist/lib/computer/lease.d.ts.map +1 -0
  110. package/dist/lib/computer/lease.js +269 -0
  111. package/dist/lib/computer/types.d.ts +163 -0
  112. package/dist/lib/computer/types.d.ts.map +1 -0
  113. package/dist/lib/computer/types.js +10 -0
  114. package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
  115. package/dist/lib/docs-metadata-v2.js +6 -5
  116. package/dist/lib/exec.d.ts.map +1 -1
  117. package/dist/lib/exec.js +1 -0
  118. package/dist/lib/guarded-remove.d.ts.map +1 -1
  119. package/dist/lib/guarded-remove.js +5 -4
  120. package/dist/lib/headless/index.d.ts +33 -0
  121. package/dist/lib/headless/index.d.ts.map +1 -1
  122. package/dist/lib/headless/index.js +38 -2
  123. package/dist/lib/http/client.d.ts +2 -9
  124. package/dist/lib/http/client.d.ts.map +1 -1
  125. package/dist/lib/http/client.js +30 -9
  126. package/dist/lib/instructions/apply.d.ts +9 -0
  127. package/dist/lib/instructions/apply.d.ts.map +1 -1
  128. package/dist/lib/instructions/apply.js +92 -14
  129. package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
  130. package/dist/lib/instructions/git-hooks.js +8 -4
  131. package/dist/lib/instructions/templates.d.ts +11 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +81 -1
  134. package/package.json +5 -5
  135. package/schemas/config.schema.json +89 -1
  136. package/src/commander.ts +98 -8
  137. package/src/commands/agents.ts +100 -32
  138. package/src/commands/autoupdate.ts +250 -0
  139. package/src/commands/computer.ts +682 -0
  140. package/src/commands/docs.ts +3 -1
  141. package/src/commands/fetch.ts +11 -1
  142. package/src/commands/grep.ts +274 -59
  143. package/src/commands/init.ts +17 -7
  144. package/src/commands/rm.ts +2 -2
  145. package/src/commands/web.ts +2 -0
  146. package/src/core/agents/canonical-emit.ts +2 -3
  147. package/src/core/agents/coord-bin.ts +30 -0
  148. package/src/core/agents/coord-client.ts +6 -2
  149. package/src/core/agents/render/prompt-context.ts +152 -53
  150. package/src/core/agents/rules/claim-conflict.ts +50 -18
  151. package/src/core/agents/rules/stop-hook.ts +28 -1
  152. package/src/core/agents/state/proc-start.ts +29 -5
  153. package/src/core/autoupdate/engine.ts +691 -0
  154. package/src/core/autoupdate/fingerprint.ts +89 -0
  155. package/src/core/autoupdate/git.ts +120 -0
  156. package/src/core/autoupdate/index.ts +29 -0
  157. package/src/core/autoupdate/state.ts +269 -0
  158. package/src/core/autoupdate/types.ts +135 -0
  159. package/src/core/config.ts +142 -1
  160. package/src/core/events/v3/bootstrap.ts +1 -0
  161. package/src/core/events/v3/runtime-build.ts +1 -0
  162. package/src/core/hooks/adapter/wiring.ts +51 -6
  163. package/src/core/hooks/autoupdate-cue.ts +72 -0
  164. package/src/core/hooks/cli.ts +54 -1
  165. package/src/core/hooks/effects/index.ts +4 -4
  166. package/src/core/hooks/resolve/coord-root.ts +1 -0
  167. package/src/core/hooks/resolve/transcript.ts +61 -0
  168. package/src/core/semantic/service.ts +6 -2
  169. package/src/core/storage/modes.ts +7 -1
  170. package/src/core/workflow/durable-record.ts +15 -1
  171. package/src/core/workflow/engine.ts +3 -0
  172. package/src/core/workflow/spawn-claude.ts +16 -4
  173. package/src/core/workflow/specialists.ts +1 -0
  174. package/src/core/workflow/types.ts +10 -0
  175. package/src/core/workflow/workspaces/leases.ts +1 -9
  176. package/src/lib/computer/cua.ts +572 -0
  177. package/src/lib/computer/host.ts +87 -0
  178. package/src/lib/computer/lease.ts +299 -0
  179. package/src/lib/computer/types.ts +177 -0
  180. package/src/lib/docs-metadata-v2.ts +24 -5
  181. package/src/lib/exec.ts +1 -0
  182. package/src/lib/guarded-remove.ts +5 -3
  183. package/src/lib/headless/index.ts +66 -2
  184. package/src/lib/http/client.ts +33 -19
  185. package/src/lib/instructions/apply.ts +100 -12
  186. package/src/lib/instructions/git-hooks.ts +8 -4
  187. package/src/lib/instructions/templates.ts +95 -1
@@ -32,7 +32,27 @@ import { resolveSearchEngine } from "../lib/tools/ripgrep.js";
32
32
  * envelope `{ pattern, mode, engine, and_patterns, without_patterns, repos,
33
33
  * total_matches, total_files, truncated, elapsed_ms }`. Matches are sorted
34
34
  * (file, then line) for stable output across runs and engines.
35
+ *
36
+ * Output and paging: a search shows every result row unless `--limit <n>`
37
+ * sets a page size, and clips each printed line to DEFAULT_MAX_COLUMNS
38
+ * characters (`--max-columns 0` turns clipping off). 500 columns keeps any
39
+ * hand-written source line and long prose lines while cutting minified or
40
+ * generated ones; on the v2 tune split a 300-column clip lost a claim that
41
+ * 500 kept, at about 8% more tokens than 300. There is
42
+ * no default row bound: on the context bench's v2 tune split, every default
43
+ * from 50 to 300 rows lost answer evidence that the unbounded search found
44
+ * (plan 2026-10-31_context-round-trips, Track D). When rows are left out, a count pass
45
+ * (the same search with `-c`, which prints one short row per file) supplies
46
+ * the totals, and the last line states how many matches and files were left
47
+ * out and the `--page <n>` that continues. Pages are cut from one global order
48
+ * (repo order, then file path, then line), so `--page 2` starts exactly where
49
+ * page 1 stopped, whichever engine ran. A content page is fetched by searching
50
+ * only the files that hold its rows.
35
51
  */
52
+ /** Characters kept per printed line unless --max-columns says otherwise. */
53
+ export const DEFAULT_MAX_COLUMNS = 500;
54
+ /** Explicit file arguments per engine call when fetching a content page. */
55
+ const PAGE_FILE_BATCH = 200;
36
56
  const DEFAULT_EXCLUDE_DIRS = [
37
57
  ".git",
38
58
  ".cache",
@@ -90,7 +110,9 @@ export function registerGrepCommand(program, emit, context) {
90
110
  .option("-A, --after-context <n>", "Print N lines after each match (overrides -C's after side)")
91
111
  .option("-B, --before-context <n>", "Print N lines before each match (overrides -C's before side)")
92
112
  .option("--max-count <n>", "Stop after N matches per file")
93
- .option("--limit <n>", "Truncate output to N matches total")
113
+ .option("--limit <n>", "Show at most N result rows per page (default: every row)")
114
+ .option("--page <n>", "Show page N of --limit-sized pages (1-based; default 1; needs --limit)")
115
+ .option("--max-columns <n>", `Clip each printed line to N characters (default ${DEFAULT_MAX_COLUMNS}; 0 = no clipping)`)
94
116
  .option("--and <pattern>", "Only show files that ALSO contain this pattern (file-level, repeatable)", collect, [])
95
117
  .option("--without <pattern>", "Drop files that contain this pattern (file-level, repeatable)", collect, [])
96
118
  .option("-q, --quiet", "No output; exit 0 if any match exists, 1 if none")
@@ -167,6 +189,10 @@ function normalizeLangs(lang) {
167
189
  /** Validate numerics, resolve context sides, expand langs, enforce the compatibility matrix. */
168
190
  function normalizeOpts(opts) {
169
191
  const limit = opts.limit ? parseIntOpt("--limit", opts.limit, 1) : Number.POSITIVE_INFINITY;
192
+ const page = opts.page ? parseIntOpt("--page", opts.page, 1) : 1;
193
+ const maxColumnsRaw = opts.maxColumns !== undefined
194
+ ? parseIntOpt("--max-columns", opts.maxColumns, 0)
195
+ : DEFAULT_MAX_COLUMNS;
170
196
  const maxCount = opts.maxCount ? parseIntOpt("--max-count", opts.maxCount, 1) : undefined;
171
197
  const c = opts.context !== undefined ? parseIntOpt("-C/--context", opts.context, 0) : 0;
172
198
  const before = opts.beforeContext !== undefined
@@ -212,15 +238,21 @@ function normalizeOpts(opts) {
212
238
  [opts.filesOnly, "-l/--files-only"],
213
239
  [opts.count, "-c/--count"],
214
240
  [opts.limit, "--limit"],
241
+ [opts.page, "--page"],
215
242
  ];
216
243
  for (const [set, flag] of rejected) {
217
244
  if (set)
218
245
  throw new Error(`-q/--quiet does not combine with ${flag}`);
219
246
  }
220
247
  }
248
+ if (opts.page && !opts.limit)
249
+ throw new Error("--page needs --limit (the page size)");
221
250
  return {
222
251
  // -q only needs existence: one accepted primary row settles the exit code.
223
252
  limit: opts.quiet ? 1 : limit,
253
+ offset: opts.quiet || !Number.isFinite(limit) ? 0 : (page - 1) * limit,
254
+ page,
255
+ maxColumns: maxColumnsRaw === 0 ? Number.POSITIVE_INFINITY : maxColumnsRaw,
224
256
  maxCount,
225
257
  before,
226
258
  after,
@@ -240,54 +272,110 @@ export async function runGrep(pattern, paths, opts, context) {
240
272
  // Host-injected default excludes (generated mirrors, vendored trees, ...)
241
273
  // ride the same --no-default-excludes gate as the built-in list.
242
274
  const hostExcludeDirs = opts.noDefaultExcludes ? [] : (context?.grepExcludeDirs ?? []);
243
- // All repos are searched concurrently; each collects at most limit+1
244
- // accepted rows (one row of lookahead, so "exactly N results" is
245
- // distinguishable from "more than N"), then the global budget is applied
246
- // in repo order below so `--limit` semantics stay deterministic.
247
- const acceptLimit = Number.isFinite(norm.limit) ? norm.limit + 1 : Number.POSITIVE_INFINITY;
248
- const perRepo = await Promise.all(repos.map((repo) => {
249
- // In --all-repos mode the parent scan prunes submodule dirs — each
250
- // submodule gets its own scoped scan, so descending from the parent
251
- // would double-scan and double-report every submodule match. This
252
- // pruning is correctness (one repo owns each match), so it applies
253
- // even when --no-default-excludes is set.
275
+ // In --all-repos mode the parent scan prunes submodule dirs — each
276
+ // submodule gets its own scoped scan, so descending from the parent would
277
+ // double-scan and double-report every submodule match. This pruning is
278
+ // correctness (one repo owns each match), so it applies even when
279
+ // --no-default-excludes is set.
280
+ const extraDirsFor = (repo) => {
254
281
  const dedupeDirs = opts.allRepos && repo.name === "parent" ? (context?.submodules ?? []) : [];
255
- const extraDirs = [...hostExcludeDirs, ...dedupeDirs];
256
- return runGrepInRepo(pattern, paths, opts, norm, repo.cwd, repo.name, acceptLimit, engine, rgBin, extraDirs);
257
- }));
282
+ return [...hostExcludeDirs, ...dedupeDirs];
283
+ };
284
+ const scan = (repo, scanPaths, scanOpts, scanNorm, acceptLimit) => runGrepInRepo(pattern, scanPaths, scanOpts, scanNorm, repo.cwd, repo.name, acceptLimit, engine, rgBin, extraDirsFor(repo));
285
+ const byPosition = (a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line;
286
+ // Every path yields, per repo, the selected rows (sorted) plus what the
287
+ // whole search found there, so the page window can be stated exactly.
288
+ let selected;
289
+ let available;
290
+ let after; // rows exist in this repo after the page window
291
+ if (opts.quiet) {
292
+ // Existence only: one accepted row settles the exit code, so the engine is
293
+ // stopped at the first hit and no count pass runs.
294
+ selected = await Promise.all(repos.map((repo) => scan(repo, paths, opts, norm, 1)));
295
+ available = selected.map((rows) => ({
296
+ matches: rows.length,
297
+ files: new Set(rows.map((m) => m.file)),
298
+ }));
299
+ after = selected.map(() => false);
300
+ }
301
+ else if (!Number.isFinite(norm.limit) || opts.files || opts.filesOnly || opts.count) {
302
+ // No page size, or a mode whose rows are files (one short row each): collect the
303
+ // complete result, then cut the page from its sorted order.
304
+ const all = await Promise.all(repos.map((repo) => scan(repo, paths, opts, norm, Number.POSITIVE_INFINITY)));
305
+ for (const rows of all)
306
+ rows.sort(byPosition);
307
+ available = all.map((rows) => ({
308
+ matches: rows.length,
309
+ files: new Set(rows.map((m) => m.file)),
310
+ }));
311
+ const windows = pageWindows(all.map((rows) => rows.map(() => 1)), norm.offset, norm.limit);
312
+ selected = all.map((rows, i) => rows.slice(windows[i]?.start ?? 0, windows[i]?.end ?? 0));
313
+ after = all.map((rows, i) => (windows[i]?.end ?? 0) < rows.length);
314
+ }
315
+ else {
316
+ // Content page. The count pass reports one row per file (path + matching
317
+ // line count) for the whole search; the page window is computed from those
318
+ // counts, and only the files holding the window's rows are searched again.
319
+ const countOpts = { ...opts, count: true };
320
+ const counts = await Promise.all(repos.map((repo) => scan(repo, paths, countOpts, norm, Number.POSITIVE_INFINITY)));
321
+ for (const rows of counts)
322
+ rows.sort(byPosition);
323
+ available = counts.map((rows) => ({
324
+ matches: rows.reduce((sum, r) => sum + r.line, 0),
325
+ files: new Set(rows.map((r) => r.file)),
326
+ }));
327
+ // Expand per-file counts into a row index space without materializing rows.
328
+ const windows = pageWindows(counts.map((rows) => rows.map((r) => r.line)), norm.offset, norm.limit);
329
+ // The selected files already passed --and/--without, so the page fetch
330
+ // skips the membership scans.
331
+ const fetchNorm = { ...norm, andPatterns: [], withoutPatterns: [] };
332
+ selected = await Promise.all(repos.map(async (repo, i) => {
333
+ const w = windows[i];
334
+ const rows = counts[i] ?? [];
335
+ if (!w || w.start >= w.end)
336
+ return [];
337
+ const picks = sliceFiles(rows, w.start, w.end);
338
+ const fetched = [];
339
+ for (let b = 0; b < picks.length; b += PAGE_FILE_BATCH) {
340
+ const batch = picks.slice(b, b + PAGE_FILE_BATCH);
341
+ fetched.push(...(await scan(repo, batch.map((p) => p.file), opts, fetchNorm, Number.POSITIVE_INFINITY)));
342
+ }
343
+ fetched.sort(byPosition);
344
+ const out = [];
345
+ for (const pick of picks) {
346
+ const inFile = fetched.filter((m) => m.file === pick.file);
347
+ out.push(...inFile.slice(pick.skip, pick.skip + pick.take));
348
+ }
349
+ return out;
350
+ }));
351
+ after = counts.map((_, i) => (windows[i]?.end ?? 0) < (available[i]?.matches ?? 0));
352
+ }
258
353
  const allRepoResults = [];
259
354
  let totalMatches = 0;
260
355
  const filesSeen = new Set();
261
- let truncated = false;
262
- let budget = norm.limit;
356
+ let availableMatches = 0;
357
+ let availableFiles = 0;
263
358
  for (let i = 0; i < repos.length; i++) {
264
359
  const repo = repos[i];
265
360
  if (!repo)
266
361
  continue;
267
- const collected = perRepo[i] ?? [];
268
- collected.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
269
- const take = Number.isFinite(budget)
270
- ? Math.min(collected.length, Math.max(0, budget))
271
- : collected.length;
272
- let matches = collected.slice(0, take);
273
- // truncated only when an accepted primary row was actually omitted —
274
- // the lookahead row (or a later-repo surplus) is the proof.
275
- const repoTruncated = collected.length > take;
276
- if (repoTruncated)
277
- truncated = true;
278
- if (Number.isFinite(budget))
279
- budget -= take;
362
+ let matches = selected[i] ?? [];
280
363
  totalMatches += matches.length;
364
+ availableMatches += available[i]?.matches ?? 0;
365
+ availableFiles += available[i]?.files.size ?? 0;
281
366
  for (const m of matches)
282
367
  filesSeen.add(`${repo.name}/${m.file}`);
283
- // Context is materialized only for the SELECTED rows, after budgeting,
284
- // so context rows never consume the limit and trailing context is never
285
- // cut off by an engine kill.
368
+ // Context is materialized only for the SELECTED rows, after paging, so
369
+ // context rows never consume the limit and trailing context is never cut
370
+ // off by an engine kill.
286
371
  if (!opts.files && !opts.filesOnly && !opts.count && (norm.before > 0 || norm.after > 0)) {
287
372
  matches = await materializeContext(matches, repo.cwd, norm.before, norm.after);
288
373
  }
289
- allRepoResults.push({ name: repo.name, cwd: repo.cwd, matches, truncated: repoTruncated });
374
+ if (Number.isFinite(norm.maxColumns))
375
+ matches = matches.map((m) => clip(m, norm.maxColumns));
376
+ allRepoResults.push({ name: repo.name, cwd: repo.cwd, matches, truncated: after[i] ?? false });
290
377
  }
378
+ const truncated = allRepoResults.some((r) => r.truncated);
291
379
  return {
292
380
  pattern,
293
381
  mode: opts.files ? "files" : opts.literal ? "literal" : "regex",
@@ -298,9 +386,58 @@ export async function runGrep(pattern, paths, opts, context) {
298
386
  total_matches: totalMatches,
299
387
  total_files: filesSeen.size,
300
388
  truncated,
389
+ limit: Number.isFinite(norm.limit) && !opts.quiet ? norm.limit : null,
390
+ page: opts.quiet ? 1 : norm.page,
391
+ available_matches: availableMatches,
392
+ available_files: availableFiles,
393
+ omitted_matches: availableMatches - totalMatches,
394
+ omitted_files: availableFiles - filesSeen.size,
395
+ next_page: truncated && !opts.quiet ? norm.page + 1 : null,
301
396
  elapsed_ms: Date.now() - started,
302
397
  };
303
398
  }
399
+ /**
400
+ * Cut the page window [offset, offset + limit) out of one global row order:
401
+ * repos in order, each repo's rows in sorted order. `weights[i][j]` is how many
402
+ * rows item j of repo i stands for (1 for a row, a file's count in the count
403
+ * pass). Returns each repo's window as row indices local to that repo.
404
+ */
405
+ function pageWindows(weights, offset, limit) {
406
+ const end = offset + limit;
407
+ let base = 0;
408
+ return weights.map((items) => {
409
+ const size = items.reduce((sum, w) => sum + w, 0);
410
+ const start = Math.min(size, Math.max(0, offset - base));
411
+ const stop = Math.min(size, Math.max(0, end - base));
412
+ base += size;
413
+ return { start, end: stop };
414
+ });
415
+ }
416
+ /** Which files (and which of their rows) a local row window covers. */
417
+ function sliceFiles(counts, start, end) {
418
+ const picks = [];
419
+ let pos = 0;
420
+ for (const row of counts) {
421
+ const fileStart = pos;
422
+ const fileEnd = pos + row.line;
423
+ pos = fileEnd;
424
+ if (fileEnd <= start)
425
+ continue;
426
+ if (fileStart >= end)
427
+ break;
428
+ const skip = Math.max(0, start - fileStart);
429
+ const take = Math.min(fileEnd, end) - fileStart - skip;
430
+ if (take > 0)
431
+ picks.push({ file: row.file, skip, take });
432
+ }
433
+ return picks;
434
+ }
435
+ /** Clip a row's text to `max` characters, recording how many were removed. */
436
+ function clip(m, max) {
437
+ if (m.text.length <= max)
438
+ return m;
439
+ return { ...m, text: m.text.slice(0, max), clipped: m.text.length - max };
440
+ }
304
441
  function resolveRepos(opts, context) {
305
442
  const repoRoot = context?.repoRoot;
306
443
  const submodules = context?.submodules;
@@ -773,7 +910,8 @@ async function materializeContext(selected, cwd, before, after) {
773
910
  function normalizeFile(file) {
774
911
  return file.startsWith("./") ? file.slice(2) : file;
775
912
  }
776
- function renderResult(r, opts) {
913
+ /** Exported for tests (not part of the package exports map). */
914
+ export function renderResult(r, opts) {
777
915
  const lines = [];
778
916
  const showRepoHeader = r.repos.length > 1;
779
917
  for (const repo of r.repos) {
@@ -797,27 +935,42 @@ function renderResult(r, opts) {
797
935
  else if (opts.count) {
798
936
  lines.push(`${m.file}:${m.line}`);
799
937
  }
800
- else if (m.kind === "context") {
801
- lines.push(`${m.file}-${m.line}-${m.text}`);
802
- }
803
938
  else {
804
- lines.push(`${m.file}:${m.line}:${m.text}`);
939
+ const sep = m.kind === "context" ? "-" : ":";
940
+ const more = m.clipped ? ` …[+${m.clipped} chars]` : "";
941
+ lines.push(`${m.file}${sep}${m.line}${sep}${m.text}${more}`);
805
942
  }
806
943
  prev = m;
807
944
  }
808
- if (repo.truncated) {
809
- lines.push(" (truncated; raise --limit)");
810
- }
811
945
  }
812
- if (r.total_matches === 0) {
946
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
947
+ const matchWord = (n) => `${n} match${n === 1 ? "" : "es"}`;
948
+ const fileRows = Boolean(opts.files || opts.filesOnly || opts.count);
949
+ if (r.total_matches === 0 && r.available_matches === 0) {
813
950
  const modeTag = r.mode === "literal" ? " (literal)" : r.mode === "files" ? " (files)" : "";
814
951
  lines.push(`(no matches for /${r.pattern}/${modeTag})`);
815
952
  }
816
- else if (r.repos.length > 1 || r.truncated) {
953
+ else if (r.omitted_matches > 0) {
954
+ // Something was left out: state the window, what was left out, and how to
955
+ // continue. Never printed when the page shows everything.
956
+ const first = r.limit === null ? 1 : (r.page - 1) * r.limit + 1;
957
+ const last = first + r.total_matches - 1;
958
+ const shown = r.total_matches === 0
959
+ ? `Page ${r.page} is past the end`
960
+ : fileRows
961
+ ? `Showing ${first}-${last} of ${plural(r.available_matches, "file")}`
962
+ : `Showing matches ${first}-${last} of ${r.available_matches} (in ${plural(r.available_files, "file")})`;
963
+ const left = fileRows
964
+ ? `${plural(r.omitted_matches, "file")} left out`
965
+ : `${matchWord(r.omitted_matches)} in ${plural(r.omitted_files, "other file")} left out`;
966
+ const next = r.next_page !== null ? `. Next: --page ${r.next_page}` : "";
967
+ lines.push("");
968
+ lines.push(`${shown}; ${left}${next}. (${r.elapsed_ms}ms, ${r.engine})`);
969
+ }
970
+ else if (r.repos.length > 1) {
817
971
  lines.push("");
818
- const summary = `${r.total_matches} match${r.total_matches === 1 ? "" : "es"} across ${r.total_files} file${r.total_files === 1 ? "" : "s"}`;
819
- const tail = r.truncated ? " (truncated; raise --limit)" : "";
820
- lines.push(`${summary}${tail} (${r.elapsed_ms}ms, ${r.engine})`);
972
+ const summary = `${matchWord(r.total_matches)} across ${plural(r.total_files, "file")}`;
973
+ lines.push(`${summary} (${r.elapsed_ms}ms, ${r.engine})`);
821
974
  }
822
975
  return lines.join("\n");
823
976
  }
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AASnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAOxE,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAKlG,OAAO,EAcL,KAAK,YAAY,EAEjB,KAAK,aAAa,EACnB,MAAM,iCAAiC,CAAC;AA0CzC;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,SAAS,EAAE,GACtB;IAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAM3C;AA6FD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAmU/F;AAiBD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAiBjF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAOvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAmEvE;AA2GD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAa1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmDhG;AASD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkCxF"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAUnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAOxE,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAKlG,OAAO,EAeL,KAAK,YAAY,EAEjB,KAAK,aAAa,EACnB,MAAM,iCAAiC,CAAC;AA0CzC;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,SAAS,EAAE,GACtB;IAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAM3C;AA8FD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAsU/F;AAkBD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAiBjF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAOvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAmEvE;AA2GD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAa1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmDhG;AASD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkCxF"}
@@ -24,14 +24,14 @@ import { createHash } from "node:crypto";
24
24
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
25
  import { dirname, relative, resolve } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
- import { codexWindowsBridgeConfig, DEFAULT_BIN_NAME, pinnedBinName, stripJsonComments, } from "../core/config.js";
27
+ import { codexWindowsBridgeConfig, DEFAULT_BIN_NAME, pinnedBinName, portableHookCommands, stripJsonComments, } from "../core/config.js";
28
28
  import { canonicalJsonV3 } from "../core/events/v3/canonical.js";
29
29
  import { ADAPTER_CAPABILITY_PROFILES_V3 } from "../core/events/v3/capabilities.js";
30
30
  import { EVENT_V3_SCHEMA_DIGEST } from "../core/events/v3/generated.js";
31
31
  import { initializeEventLedgerV3, readEventV3ControlState, sha256V3, } from "../core/events/v3/index.js";
32
32
  import { ADAPTER_SPECS } from "../core/hooks/adapter/events.js";
33
33
  import { checkOpenCodePlugin, installOpenCodePlugin, } from "../core/hooks/adapter/opencode-plugin.js";
34
- import { agentHookPathForProject, applyCursorStopLoopLimit, CURSOR_STOP_FOLLOWUP_LOOP_LIMIT, commandWiresSubcommand, diffWiring, expectedCommandWindows, groupCommands, hookCommand, hookTimeoutFor, isAgentHookCommand, makeEntry, orderHandlerKeys, summarizeAdapterWiring, } from "../core/hooks/adapter/wiring.js";
34
+ import { agentHookPathForProject, applyCursorStopLoopLimit, CURSOR_STOP_FOLLOWUP_LOOP_LIMIT, commandWiresSubcommand, diffWiring, expectedCommandWindows, groupCommands, hookCommand, hookTimeoutFor, isAgentHookCommand, makeEntry, orderHandlerKeys, projectWiringOptions, summarizeAdapterWiring, } from "../core/hooks/adapter/wiring.js";
35
35
  import { detectInstalledAdapters } from "../core/workflow/adapters.js";
36
36
  import { applyIndexerExclusions } from "../lib/indexer-exclusions.js";
37
37
  import { applyInstructions, checkInstructions, } from "../lib/instructions/apply.js";
@@ -77,7 +77,8 @@ export function resolveInitAdapters(projectRoot, requested, installed) {
77
77
  /** Drift check for one adapter's runtime wiring (settings hooks or the OpenCode plugin). */
78
78
  function checkAdapterHooks(projectRoot, adapter, bin) {
79
79
  const spec = ADAPTER_SPECS[adapter];
80
- const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT);
80
+ const options = projectWiringOptions(projectRoot);
81
+ const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT, options);
81
82
  if (spec.installMode === "opencode-plugin") {
82
83
  // No hooks map to diff: the plugin directory is the wiring.
83
84
  const plugin = checkOpenCodePlugin(projectRoot, { binName: bin, agentHook });
@@ -105,7 +106,7 @@ function checkAdapterHooks(projectRoot, adapter, bin) {
105
106
  const hookDiff = diffWiring(settings, spec, {
106
107
  agentHookPath: agentHook,
107
108
  adapter,
108
- windowsBridgeEntryPoint: codexWindowsBridgeConfig(projectRoot).entryPoint,
109
+ ...options,
109
110
  });
110
111
  drift =
111
112
  hookDiff.missing.length > 0 ||
@@ -354,7 +355,8 @@ export function registerInitCommand(program, emit, binName) {
354
355
  for (const adapter of adapters) {
355
356
  const spec = ADAPTER_SPECS[adapter];
356
357
  const settingsPath = resolve(projectRoot, spec.settingsFile);
357
- const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT);
358
+ const portableCommands = portableHookCommands(projectRoot);
359
+ const agentHook = agentHookPathForProject(projectRoot, HARNERY_ROOT, { portableCommands });
358
360
  if (spec.installMode === "opencode-plugin") {
359
361
  // OpenCode has no hooks map. Install the plugin directory instead; the
360
362
  // instructions + skills step below still runs (AGENTS.md and
@@ -385,9 +387,9 @@ export function registerInitCommand(program, emit, binName) {
385
387
  }
386
388
  const bridge = codexWindowsBridgeConfig(projectRoot);
387
389
  if (bridge.error && spec.windowsBridgeHooks) {
388
- actions.push(`⚠ ${bridge.error}; writing ${adapter} hooks without commandWindows`);
390
+ actions.push(`⚠ ${bridge.error}; writing ${adapter} hooks without the Windows bridge command`);
389
391
  }
390
- const { wired, already, removed, upgraded } = wireHooks(settings, spec, agentHook, adapter, { windowsBridgeEntryPoint: bridge.entryPoint });
392
+ const { wired, already, removed, upgraded } = wireHooks(settings, spec, agentHook, adapter, { windowsBridgeEntryPoint: bridge.entryPoint, portableCommands });
391
393
  if (wired === 0 && removed === 0 && upgraded === 0) {
392
394
  actions.push(`· all ${spec.events.length} ${adapter} hooks already wired in ${rel(projectRoot, settingsPath)}`);
393
395
  }
@@ -440,6 +442,7 @@ export function registerInitCommand(program, emit, binName) {
440
442
  }
441
443
  function gitBuild(root) {
442
444
  const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], {
445
+ windowsHide: true,
443
446
  encoding: "utf8",
444
447
  timeout: 5000,
445
448
  });
@@ -505,7 +508,7 @@ export function wireHooks(settings, spec, agentHookPath, adapter, options) {
505
508
  }
506
509
  }
507
510
  for (const { settingsKey, subcommand } of spec.events) {
508
- const command = hookCommand(spec, agentHookPath, subcommand, adapter);
511
+ const command = hookCommand(spec, agentHookPath, subcommand, adapter, options);
509
512
  const timeout = hookTimeoutFor(spec, subcommand);
510
513
  const commandWindows = expectedCommandWindows(spec, agentHookPath, subcommand, adapter, options);
511
514
  const handlerFields = {
@@ -836,7 +839,10 @@ function renderInstructionsOnly(projectRoot, binName, dryRun, actions, warnings
836
839
  return `${head}\n root: ${projectRoot}\n${actions.map((action) => ` ${action}`).join("\n")}${warnBlock}${tail}`;
837
840
  }
838
841
  function gitTopLevel() {
839
- const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
842
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], {
843
+ encoding: "utf8",
844
+ windowsHide: true,
845
+ });
840
846
  const out = r.status === 0 ? r.stdout.trim() : "";
841
847
  return out || null;
842
848
  }
@@ -11,11 +11,11 @@ export function registerRmCommand(program, emit, context) {
11
11
  program
12
12
  .command("rm <paths...>")
13
13
  .description("Preview guarded removal of untracked files; --yes applies permanent deletion")
14
- .requiredOption("--root <directory>", "Allowed directory; each target must be strictly inside it")
14
+ .requiredOption("--root <directory>", "Allowed directory; relative targets resolve under it, and each target must be strictly inside it")
15
15
  .option("-r, --recursive", "Permit directory removal after inspecting its complete contents")
16
16
  .option("--clean-clone", "Remove only clean standalone clones inside one owned artifact; refuse local-only commits and worktrees")
17
17
  .option("--entry-limit <n>", "Maximum inspected entries, 1-100000 (default 10000); larger limits require one current caller-owned managed artifact as --root", (value) => Number(value))
18
- .option("--allow-protected <path>", "Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; repeat per path", (value, previous = []) => [...previous, value])
18
+ .option("--allow-protected <path>", "Remove this one protected path (Git metadata, credentials, Harnery state, keys) inside a target; relative paths resolve under --root; repeat per path", (value, previous = []) => [...previous, value])
19
19
  .option("--yes", "Permanently remove validated targets")
20
20
  .option("--dry-run", "Preview only, even with --yes")
21
21
  .option("--json", "Emit the versioned removal report")
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/commands/web.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA8CnD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,MAAM,CAQR;AAmDD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAevE;AAsBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA2N5E"}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/commands/web.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA8CnD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,MAAM,CAQR;AAmDD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAevE;AAsBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA6N5E"}
@@ -234,6 +234,7 @@ export function registerWebCommand(program, emit) {
234
234
  if (heapMb > 0)
235
235
  emit.log(`heap ceiling · ${heapMb} MB`, "info");
236
236
  const child = spawn(webRunner(), ["run", mode], {
237
+ windowsHide: true,
237
238
  cwd: root,
238
239
  env: {
239
240
  ...process.env,
@@ -324,6 +325,7 @@ export function registerWebCommand(program, emit) {
324
325
  if (heapMb > 0)
325
326
  emit.log(`heap ceiling · ${heapMb} MB`, "info");
326
327
  const child = spawn(webRunner(), ["run", "start"], {
328
+ windowsHide: true,
327
329
  cwd: root,
328
330
  env: {
329
331
  ...process.env,
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,KAAK,gBAAgB,EAAgC,MAAM,4BAA4B,CAAC;AACjG,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AAGtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,sBAAsB,GAC9B,6BAA6B,GAC7B;IACE,UAAU,EAAE,yBAAyB,CAAC;IACtC,SAAS,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEN,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CA4C5D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAE/E;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,mBAAmB,GAAG,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3E,QAAQ,EAAE,MAAM,GACf,MAAM,CAQR"}
1
+ {"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAgC,MAAM,4BAA4B,CAAC;AACjG,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AAGtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,sBAAsB,GAC9B,6BAA6B,GAC7B;IACE,UAAU,EAAE,yBAAyB,CAAC;IACtC,SAAS,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEN,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CA4C5D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAE/E;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,mBAAmB,GAAG,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3E,QAAQ,EAAE,MAAM,GACf,MAAM,CAQR"}
@@ -9,9 +9,8 @@
9
9
  * a 15-second safety budget; loaded-host checks normally finish in a few
10
10
  * seconds and record the observation before the parent command exits.
11
11
  */
12
- import { spawnSync } from "node:child_process";
13
12
  import { eventAdapterIdV3FromPlatform } from "../events/v3/adapter-id.js";
14
- import { coordBinPath } from "./coord-bin.js";
13
+ import { coordBinPath, spawnCoordBinSync } from "./coord-bin.js";
15
14
  import { resolveCoordRoot } from "./coord-client.js";
16
15
  /**
17
16
  * Resolve the coord root for a canonical emit.
@@ -45,7 +44,7 @@ export function emitEventV3(input) {
45
44
  ];
46
45
  // Pin cwd + HARNERY_COORD_ROOT_OVERRIDE so the child writes to the SAME
47
46
  // stream we resolved, regardless of where the caller's shell is cd'd.
48
- const result = spawnSync(binary, args, {
47
+ const result = spawnCoordBinSync(binary, args, {
49
48
  encoding: "utf8",
50
49
  stdio: ["pipe", "ignore", "pipe"],
51
50
  input: JSON.stringify(input.observation),
@@ -20,7 +20,10 @@
20
20
  * host whose harnery copy is not the one executing (a vendored tree, a shim)
21
21
  * still resolves.
22
22
  */
23
+ import { type SpawnSyncOptions, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process";
23
24
  export type CoordBinName = "agent-coord" | "agent-hook" | "harn";
25
+ export declare function spawnCoordBinSync(binary: string, args: readonly string[], options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns<string>;
26
+ export declare function spawnCoordBinSync(binary: string, args: readonly string[], options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer>;
24
27
  /**
25
28
  * Absolute path to one of harnery's bundled binaries, or null when no candidate
26
29
  * exists on disk. Callers must handle null — a missing helper is a real state
@@ -1 +1 @@
1
- {"version":3,"file":"coord-bin.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-bin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKzF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAuB1F"}
1
+ {"version":3,"file":"coord-bin.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-bin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,kCAAkC,EACvC,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AAM5B,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAEjE,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,kCAAkC,GAC1C,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAC5B,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;AAerC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKzF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAuB1F"}
@@ -20,10 +20,16 @@
20
20
  * host whose harnery copy is not the one executing (a vendored tree, a shim)
21
21
  * still resolves.
22
22
  */
23
+ import { spawnSync, } from "node:child_process";
23
24
  import { existsSync } from "node:fs";
24
25
  import { dirname, join, resolve } from "node:path";
25
26
  import { fileURLToPath } from "node:url";
26
27
  import { coordEnv } from "../../lib/env.js";
28
+ /** Execute the native launcher on Windows, preserving stdin, argv and blocking exit codes. */
29
+ export function spawnCoordBinSync(binary, args, options = {}) {
30
+ const native = process.platform === "win32";
31
+ return spawnSync(native ? process.execPath : binary, native ? [`${binary}.mjs`, ...args] : [...args], { windowsHide: true, ...options });
32
+ }
27
33
  /**
28
34
  * Absolute path to one of harnery's bundled binaries, or null when no candidate
29
35
  * exists on disk. Callers must handle null — a missing helper is a real state
@@ -1 +1 @@
1
- {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA+B7E;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtD;AAiED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2CxD;AAgCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAsEA;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsB5C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,iBAAiB,GAAG,MAAM,CAAC;CAC/C,CAyBA;AAyBD,sFAAsF;AACtF;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAuCD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAmB9C;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE"}
1
+ {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA+B7E;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtD;AAiED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4CxD;AAgCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAsEA;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsB5C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,iBAAiB,GAAG,MAAM,CAAC;CAC/C,CAyBA;AAyBD,sFAAsF;AACtF;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAuCD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAmB9C;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE"}
@@ -203,7 +203,7 @@ function gitRevParse(cwd, flag) {
203
203
  // `cwd` must exist or the spawn itself fails; callers pass paths that may
204
204
  // not (a project dir from a stale env var), so treat any failure as "no
205
205
  // answer" rather than letting it throw.
206
- const r = spawnSync("git", ["rev-parse", flag], { encoding: "utf8", cwd });
206
+ const r = spawnSync("git", ["rev-parse", flag], { encoding: "utf8", cwd, windowsHide: true });
207
207
  if (r.status === 0)
208
208
  value = r.stdout.trim() || null;
209
209
  }
@@ -287,6 +287,7 @@ export function pidStartToken(pid) {
287
287
  // two callers with different environments must not disagree about one
288
288
  // process.
289
289
  const out = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
290
+ windowsHide: true,
290
291
  encoding: "utf8",
291
292
  timeout: 2000,
292
293
  env: { ...process.env, TZ: "UTC", LC_ALL: "C" },
@@ -651,7 +652,10 @@ function readPpid(pid) {
651
652
  }
652
653
  // Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
653
654
  try {
654
- const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
655
+ const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], {
656
+ encoding: "utf8",
657
+ windowsHide: true,
658
+ });
655
659
  if (out.status === 0) {
656
660
  const parsed = Number.parseInt(out.stdout.trim(), 10);
657
661
  if (Number.isFinite(parsed) && parsed > 0)