harnery 0.7.1 → 0.9.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 (98) hide show
  1. package/README.md +2 -1
  2. package/dist/commander.d.ts +9 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +2 -0
  5. package/dist/commands/callers.d.ts.map +1 -1
  6. package/dist/commands/callers.js +69 -17
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +84 -2
  9. package/dist/commands/grep.d.ts +77 -0
  10. package/dist/commands/grep.d.ts.map +1 -1
  11. package/dist/commands/grep.js +598 -109
  12. package/dist/commands/init.d.ts +15 -5
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +125 -14
  15. package/dist/commands/workflow.d.ts +4 -0
  16. package/dist/commands/workflow.d.ts.map +1 -0
  17. package/dist/commands/workflow.js +89 -0
  18. package/dist/core/agents/cli.js +10 -3
  19. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  20. package/dist/core/agents/rules/claim-conflict.js +26 -1
  21. package/dist/core/agents/rules/stop-hook.d.ts +8 -0
  22. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  23. package/dist/core/agents/rules/stop-hook.js +8 -0
  24. package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
  25. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  26. package/dist/core/agents/state/heartbeat-projector.js +13 -2
  27. package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
  28. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  29. package/dist/core/agents/state/heartbeat-writer.js +21 -4
  30. package/dist/core/config.d.ts +18 -0
  31. package/dist/core/config.d.ts.map +1 -1
  32. package/dist/core/config.js +38 -0
  33. package/dist/core/hooks/cli.js +29 -3
  34. package/dist/core/hooks/events/schema.d.ts +4 -0
  35. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  36. package/dist/core/hooks/harness/events.d.ts +7 -0
  37. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  38. package/dist/core/hooks/harness/events.js +1 -0
  39. package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
  40. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
  41. package/dist/core/hooks/resolve/coord-root.js +28 -6
  42. package/dist/core/workflow/billing.d.ts +48 -0
  43. package/dist/core/workflow/billing.d.ts.map +1 -0
  44. package/dist/core/workflow/billing.js +102 -0
  45. package/dist/core/workflow/child-env.d.ts +30 -0
  46. package/dist/core/workflow/child-env.d.ts.map +1 -0
  47. package/dist/core/workflow/child-env.js +43 -0
  48. package/dist/core/workflow/engine.d.ts +21 -0
  49. package/dist/core/workflow/engine.d.ts.map +1 -0
  50. package/dist/core/workflow/engine.js +340 -0
  51. package/dist/core/workflow/harnesses.d.ts +17 -0
  52. package/dist/core/workflow/harnesses.d.ts.map +1 -0
  53. package/dist/core/workflow/harnesses.js +30 -0
  54. package/dist/core/workflow/spawn-claude.d.ts +22 -0
  55. package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
  56. package/dist/core/workflow/spawn-claude.js +82 -0
  57. package/dist/core/workflow/spawn-codex.d.ts +19 -0
  58. package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
  59. package/dist/core/workflow/spawn-codex.js +66 -0
  60. package/dist/core/workflow/spawn-cursor.d.ts +26 -0
  61. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
  62. package/dist/core/workflow/spawn-cursor.js +72 -0
  63. package/dist/core/workflow/types.d.ts +145 -0
  64. package/dist/core/workflow/types.d.ts.map +1 -0
  65. package/dist/core/workflow/types.js +9 -0
  66. package/dist/core/workflow/validate.d.ts +15 -0
  67. package/dist/core/workflow/validate.d.ts.map +1 -0
  68. package/dist/core/workflow/validate.js +70 -0
  69. package/dist/lib/tools/ripgrep.d.ts +61 -0
  70. package/dist/lib/tools/ripgrep.d.ts.map +1 -0
  71. package/dist/lib/tools/ripgrep.js +217 -0
  72. package/package.json +1 -1
  73. package/src/commander.ts +11 -0
  74. package/src/commands/callers.ts +90 -26
  75. package/src/commands/doctor.ts +95 -2
  76. package/src/commands/grep.ts +737 -113
  77. package/src/commands/init.ts +138 -17
  78. package/src/commands/workflow.ts +132 -0
  79. package/src/core/agents/cli.ts +11 -4
  80. package/src/core/agents/rules/claim-conflict.ts +26 -1
  81. package/src/core/agents/rules/stop-hook.ts +17 -0
  82. package/src/core/agents/state/heartbeat-projector.ts +13 -1
  83. package/src/core/agents/state/heartbeat-writer.ts +30 -5
  84. package/src/core/config.ts +47 -0
  85. package/src/core/hooks/cli.ts +30 -3
  86. package/src/core/hooks/events/schema.ts +4 -0
  87. package/src/core/hooks/harness/events.ts +8 -0
  88. package/src/core/hooks/resolve/coord-root.ts +28 -6
  89. package/src/core/workflow/billing.ts +146 -0
  90. package/src/core/workflow/child-env.ts +47 -0
  91. package/src/core/workflow/engine.ts +394 -0
  92. package/src/core/workflow/harnesses.ts +38 -0
  93. package/src/core/workflow/spawn-claude.ts +99 -0
  94. package/src/core/workflow/spawn-codex.ts +74 -0
  95. package/src/core/workflow/spawn-cursor.ts +89 -0
  96. package/src/core/workflow/types.ts +153 -0
  97. package/src/core/workflow/validate.ts +75 -0
  98. package/src/lib/tools/ripgrep.ts +244 -0
package/README.md CHANGED
@@ -20,10 +20,11 @@
20
20
  `harnery` keeps multiple AI coding agents from stepping on each other in a shared checkout. It was extracted from years of running several Claude Code / Cursor / Codex sessions at once against the same monorepo:
21
21
 
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
+ - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages.
23
24
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
24
25
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
25
26
 
26
- The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`, `grep`, `docs`, `browse`, `fetch`, `read`, `devtools`, and more) plus the library toolkit they're built from. Useful, cross-platform, dependency-light — and deliberately not the headline. Coordination is why harnery exists; the toolkit exists because the CLI needed it (see [Public surface tiers](#public-surface-tiers)).
27
+ The CLI also ships batteries: portable utility commands (`tokens`, `eml`, `env`, `grep` — parallel, ripgrep-accelerated monorepo search — `docs`, `browse`, `fetch`, `read`, `devtools`, and more) plus the library toolkit they're built from. Useful, cross-platform, dependency-light — and deliberately not the headline. Coordination is why harnery exists; the toolkit exists because the CLI needed it (see [Public surface tiers](#public-surface-tiers)).
27
28
 
28
29
  ## Install
29
30
 
@@ -78,6 +78,15 @@ export interface HarneryProgramContext {
78
78
  * carrying provider-specific checks. harn standalone ships none.
79
79
  */
80
80
  envSections?: Record<string, EnvSection>;
81
+ /**
82
+ * Extra directory names for `grep` to exclude by default — host-generated
83
+ * mirrors, vendored trees, and similar directories that only ever produce
84
+ * duplicate or noisy matches. Names match at any depth (same semantics as
85
+ * grep's `--exclude-dir`). Applied alongside the built-in skip list and
86
+ * disabled together with it by `--no-default-excludes`. harn standalone
87
+ * ships none.
88
+ */
89
+ grepExcludeDirs?: readonly string[];
81
90
  /**
82
91
  * Optional callback that returns extra HTTP headers to attach to outbound
83
92
  * `fetch` calls based on the target URL. Useful for consumers that need
@@ -1 +1 @@
1
- {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"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"}
package/dist/commander.js CHANGED
@@ -48,6 +48,7 @@ import { registerSectionCommand, registerTocCommand } from "./commands/toc.js";
48
48
  import { registerTokensCommand } from "./commands/tokens.js";
49
49
  import { registerTunnelCommand } from "./commands/tunnel.js";
50
50
  import { registerWebCommand } from "./commands/web.js";
51
+ import { registerWorkflowCommand } from "./commands/workflow.js";
51
52
  export const defaultEmit = {
52
53
  config() {
53
54
  // No-op for harn standalone: there's only one format (JSON) and it's
@@ -122,6 +123,7 @@ export function createHarneryProgram(opts = {}) {
122
123
  registerDeinitCommand(program, emit, opts.binName);
123
124
  registerBackupCommand(program, emit);
124
125
  registerSyncCommand(program, emit);
126
+ registerWorkflowCommand(program, emit);
125
127
  if (include("web"))
126
128
  registerWebCommand(program, emit);
127
129
  return program;
@@ -1 +1 @@
1
- {"version":3,"file":"callers.d.ts","sourceRoot":"","sources":["../../src/commands/callers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAuE1E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA6BN"}
1
+ {"version":3,"file":"callers.d.ts","sourceRoot":"","sources":["../../src/commands/callers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA0E1E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA6BN"}
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { resolveSearchEngine } from "../lib/tools/ripgrep.js";
2
3
  const DEFAULT_EXCLUDE_DIRS = [
3
4
  ".git",
4
5
  ".cache",
@@ -62,20 +63,29 @@ async function runCallers(symbol, opts, context) {
62
63
  throw new Error(`symbol must be a valid identifier (got: ${symbol})`);
63
64
  }
64
65
  const started = Date.now();
66
+ const { engine, rgBin } = await resolveSearchEngine("callers");
65
67
  const repos = resolveRepos(opts, context);
66
68
  const limit = opts.limit ? Number.parseInt(opts.limit, 10) : 500;
69
+ // Search every repo concurrently, each capped at the global limit, then
70
+ // apply the global budget in repo order so truncation stays deterministic.
71
+ // In --all-repos mode the parent scan prunes submodule dirs (each submodule
72
+ // is scanned on its own), so a match is attributed to exactly one repo.
73
+ const perRepo = await Promise.all(repos.map((repo) => {
74
+ const pruneDirs = opts.allRepos && repo.name === "parent" ? (context?.submodules ?? []) : [];
75
+ return searchInRepo(symbol, opts, repo, limit, engine, rgBin, pruneDirs);
76
+ }));
67
77
  const allCallers = [];
68
78
  let truncated = false;
69
- for (const repo of repos) {
70
- if (allCallers.length >= limit) {
71
- truncated = true;
72
- break;
73
- }
74
- const remaining = limit - allCallers.length;
75
- const found = await grepInRepo(symbol, opts, repo, remaining);
79
+ let budget = limit;
80
+ for (let i = 0; i < repos.length; i++) {
81
+ const found = perRepo[i] ?? { callers: [], truncated: false };
76
82
  if (found.truncated)
77
83
  truncated = true;
78
- allCallers.push(...found.callers);
84
+ const take = Math.min(found.callers.length, Math.max(0, budget));
85
+ if (found.callers.length > take)
86
+ truncated = true;
87
+ allCallers.push(...found.callers.slice(0, take));
88
+ budget -= take;
79
89
  }
80
90
  const by_kind = {};
81
91
  for (const c of allCallers) {
@@ -121,22 +131,61 @@ function resolveRepos(opts, context) {
121
131
  }
122
132
  return [{ name: "cwd", cwd: process.cwd() }];
123
133
  }
124
- function grepInRepo(symbol, opts, repo, limit) {
125
- const args = ["-rnw", "--color=never", "-I", "-E"];
126
- for (const d of DEFAULT_EXCLUDE_DIRS)
127
- args.push(`--exclude-dir=${d}`);
128
- for (const f of DEFAULT_EXCLUDE_FILES)
129
- args.push(`--exclude=${f}`);
134
+ function resolveLangGlobs(opts) {
130
135
  const langGlobs = opts.lang ? LANG_GLOBS[opts.lang] : undefined;
131
136
  if (opts.lang && !langGlobs) {
132
137
  throw new Error(`unknown --lang "${opts.lang}". Valid: ${Object.keys(LANG_GLOBS).join(", ")}`);
133
138
  }
139
+ return langGlobs;
140
+ }
141
+ function buildCallersGrepArgs(symbol, opts, extraExcludeDirs) {
142
+ const args = ["-rnw", "-H", "--color=never", "-I", "-E"];
143
+ for (const d of DEFAULT_EXCLUDE_DIRS)
144
+ args.push(`--exclude-dir=${d}`);
145
+ for (const f of DEFAULT_EXCLUDE_FILES)
146
+ args.push(`--exclude=${f}`);
147
+ for (const d of extraExcludeDirs)
148
+ args.push(`--exclude-dir=${d}`);
149
+ const langGlobs = resolveLangGlobs(opts);
134
150
  if (langGlobs)
135
151
  for (const g of langGlobs)
136
152
  args.push(`--include=${g}`);
137
153
  args.push("--", symbol, ".");
154
+ return args;
155
+ }
156
+ function buildCallersRgArgs(symbol, opts, extraExcludeDirs) {
157
+ // --hidden --no-ignore --no-config: match grep semantics (dotdirs searched,
158
+ // .gitignore ignored, user rg config can't skew results). -w = whole word.
159
+ // Lang globs (positives) go before dir excludes (negatives) since rg globs
160
+ // are last-match-wins.
161
+ const args = [
162
+ "-nw",
163
+ "--no-heading",
164
+ "--with-filename",
165
+ "--color=never",
166
+ "--no-config",
167
+ "--hidden",
168
+ "--no-ignore",
169
+ ];
170
+ const langGlobs = resolveLangGlobs(opts);
171
+ if (langGlobs)
172
+ for (const g of langGlobs)
173
+ args.push(`--glob=${g}`);
174
+ for (const d of DEFAULT_EXCLUDE_DIRS)
175
+ args.push(`--glob=!${d}`);
176
+ for (const f of DEFAULT_EXCLUDE_FILES)
177
+ args.push(`--glob=!${f}`);
178
+ for (const d of extraExcludeDirs)
179
+ args.push(`--glob=!${d}`);
180
+ args.push("--", symbol, ".");
181
+ return args;
182
+ }
183
+ function searchInRepo(symbol, opts, repo, limit, engine, rgBin, extraExcludeDirs) {
184
+ const [bin, args] = engine === "rg"
185
+ ? [rgBin, buildCallersRgArgs(symbol, opts, extraExcludeDirs)]
186
+ : ["grep", buildCallersGrepArgs(symbol, opts, extraExcludeDirs)];
138
187
  return new Promise((resolveP, reject) => {
139
- const proc = spawn("grep", args, { cwd: repo.cwd, stdio: ["ignore", "pipe", "pipe"] });
188
+ const proc = spawn(bin, args, { cwd: repo.cwd, stdio: ["ignore", "pipe", "pipe"] });
140
189
  const callers = [];
141
190
  let buffer = "";
142
191
  let stderr = "";
@@ -174,8 +223,11 @@ function grepInRepo(symbol, opts, repo, limit) {
174
223
  if (parsed)
175
224
  callers.push(parsed);
176
225
  }
177
- if (code !== null && code !== 0 && code !== 1 && !truncated) {
178
- reject(new Error(`grep exited ${code}: ${stderr.trim() || "(no stderr)"}`));
226
+ // Exit 1 = no matches (both engines): normal. Exit 2 with nothing
227
+ // collected = a real failure; with matches, a partial walk error we
228
+ // tolerate (surface what was found).
229
+ if (code !== null && code !== 0 && code !== 1 && !truncated && callers.length === 0) {
230
+ reject(new Error(`${bin} exited ${code}: ${stderr.trim() || "(no stderr)"}`));
179
231
  return;
180
232
  }
181
233
  resolveP({ callers, truncated });
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwC/E;AAED,wBAAgB,SAAS,IAAI,KAAK,EAAE,CAYnC"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmD/E;AAED,wBAAgB,SAAS,IAAI,KAAK,EAAE,CAgBnC"}
@@ -14,15 +14,27 @@ import { spawnSync } from "node:child_process";
14
14
  import { existsSync } from "node:fs";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
- import { resolveBinName } from "../core/config.js";
17
+ import { resolveBinName, ripgrepAutoInstall } from "../core/config.js";
18
18
  import { loadHarnessWiring } from "../core/hooks/harness/wiring.js";
19
+ import { probeBilling } from "../core/workflow/billing.js";
20
+ import { HARNESS_BINARIES, HARNESS_INSTALL_HINTS, HARNESS_LOGIN_HINTS, } from "../core/workflow/harnesses.js";
21
+ import { findRg, installRg, managedRgPath, rgInstallSupported } from "../lib/tools/ripgrep.js";
19
22
  export function registerDoctorCommand(program, emit) {
20
23
  program
21
24
  .command("doctor")
22
25
  .description("Verify the runtime + optional deps harnery commands expect. Exits 0 " +
23
26
  "unless a required dep (Node, git) is missing.")
24
27
  .option("--json", "Machine-readable JSON output")
25
- .action((opts) => {
28
+ .option("--fix", "Install missing managed tools (currently: ripgrep, pinned + checksum-verified)")
29
+ .action(async (opts) => {
30
+ if (opts.fix && !findRg() && rgInstallSupported()) {
31
+ try {
32
+ await installRg((line) => emit.text(`${line}\n`));
33
+ }
34
+ catch (err) {
35
+ emit.text(`ripgrep install failed: ${err.message}\n`);
36
+ }
37
+ }
26
38
  const checks = runChecks();
27
39
  const requiredFailed = checks.some((c) => c.severity === "fail");
28
40
  if (opts.json) {
@@ -56,14 +68,51 @@ export function runChecks() {
56
68
  checkNode(),
57
69
  checkGit(),
58
70
  checkBun(),
71
+ checkRipgrep(),
59
72
  checkHarneryDir(),
60
73
  checkHarnessHooks(),
74
+ checkWorkflowHarness("claude-code"),
75
+ checkWorkflowHarness("codex"),
76
+ checkWorkflowHarness("cursor"),
61
77
  checkRestic(),
62
78
  checkRclone(),
63
79
  checkPlaywright(),
64
80
  checkPython(),
65
81
  ];
66
82
  }
83
+ function checkRipgrep() {
84
+ const found = findRg();
85
+ if (found) {
86
+ const r = whichVersion(found);
87
+ const managed = found === managedRgPath() ? ", managed" : "";
88
+ return {
89
+ name: "ripgrep",
90
+ severity: "ok",
91
+ detail: `${r.out.replace(/^ripgrep\s*/, "")} (${found === "rg" ? "PATH" : found}${managed})`,
92
+ };
93
+ }
94
+ if (!rgInstallSupported()) {
95
+ return {
96
+ name: "ripgrep",
97
+ severity: "warn",
98
+ detail: "missing (grep fallback; no pinned artifact for this OS/arch)",
99
+ hint: "https://github.com/BurntSushi/ripgrep#installation",
100
+ };
101
+ }
102
+ if (ripgrepAutoInstall()) {
103
+ return {
104
+ name: "ripgrep",
105
+ severity: "warn",
106
+ detail: "missing (autoInstall on: will self-provision on first grep)",
107
+ };
108
+ }
109
+ return {
110
+ name: "ripgrep",
111
+ severity: "warn",
112
+ detail: "missing (grep fallback works, just slower)",
113
+ hint: `${resolveBinName()} doctor --fix (pinned + checksum-verified, installs to the harnery tools dir)`,
114
+ };
115
+ }
67
116
  function whichVersion(bin, args = ["--version"]) {
68
117
  const r = spawnSync(bin, args, { encoding: "utf-8" });
69
118
  if (r.status !== 0)
@@ -71,6 +120,39 @@ function whichVersion(bin, args = ["--version"]) {
71
120
  const out = (r.stdout || r.stderr).trim().split("\n")[0];
72
121
  return { ok: true, out };
73
122
  }
123
+ /**
124
+ * One workflow spawn target: is the harness CLI installed, and how will its
125
+ * headless children bill (subscription login vs API key — see billing.ts)?
126
+ * Missing is a warn, not a fail: workflows degrade to the harnesses you have.
127
+ */
128
+ function checkWorkflowHarness(harness) {
129
+ const bin = HARNESS_BINARIES[harness];
130
+ const name = `workflow:${harness}`;
131
+ const r = whichVersion(bin);
132
+ if (!r.ok) {
133
+ return {
134
+ name,
135
+ severity: "warn",
136
+ detail: `${bin} missing (workflow --harness ${harness} unavailable)`,
137
+ hint: `${HARNESS_INSTALL_HINTS[harness]} then: ${HARNESS_LOGIN_HINTS[harness]}`,
138
+ };
139
+ }
140
+ const probe = probeBilling(harness);
141
+ if (probe.login === "absent" && !probe.apiKeyPresent) {
142
+ return {
143
+ name,
144
+ severity: "warn",
145
+ detail: `${r.out} — installed, but no stored login or API key detected`,
146
+ hint: HARNESS_LOGIN_HINTS[harness],
147
+ };
148
+ }
149
+ const billing = probe.mode === "subscription"
150
+ ? probe.login === "present"
151
+ ? "billing: subscription"
152
+ : "billing: subscription (login unverifiable, CLI is the authority)"
153
+ : `billing: ${probe.mode} (${probe.apiKeySource})`;
154
+ return { name, severity: "ok", detail: `${r.out} — ${billing}` };
155
+ }
74
156
  function checkNode() {
75
157
  const v = process.versions.node;
76
158
  const major = Number.parseInt(v.split(".")[0], 10);
@@ -1,4 +1,81 @@
1
1
  import type { Command } from "commander";
2
2
  import type { EmitContext, HarneryProgramContext } from "../commander.js";
3
+ import { type SearchEngine } from "../lib/tools/ripgrep.js";
4
+ export type GrepEngine = SearchEngine;
5
+ export interface GrepOpts {
6
+ repo?: string;
7
+ allRepos?: boolean;
8
+ /** Repeatable and/or comma-separated (`--lang ts,tsx`). A bare string is accepted for direct callers. */
9
+ lang?: string | string[];
10
+ ignoreCase?: boolean;
11
+ wholeWord?: boolean;
12
+ literal?: boolean;
13
+ filesOnly?: boolean;
14
+ files?: boolean;
15
+ count?: boolean;
16
+ context?: string;
17
+ afterContext?: string;
18
+ beforeContext?: string;
19
+ maxCount?: string;
20
+ limit?: string;
21
+ include?: string[];
22
+ exclude?: string[];
23
+ and?: string[];
24
+ without?: string[];
25
+ quiet?: boolean;
26
+ noDefaultExcludes?: boolean;
27
+ json?: boolean;
28
+ }
29
+ export interface Match {
30
+ repo: string;
31
+ file: string;
32
+ line: number;
33
+ text: string;
34
+ /** "match" = primary result row (counts toward totals + --limit); "context" = free -C/-A/-B row. */
35
+ kind: "match" | "context";
36
+ }
3
37
  export declare function registerGrepCommand(program: Command, emit: EmitContext, context?: HarneryProgramContext): void;
38
+ export interface GrepResult {
39
+ pattern: string;
40
+ mode: "regex" | "literal" | "files";
41
+ engine: GrepEngine;
42
+ and_patterns: string[];
43
+ without_patterns: string[];
44
+ repos: {
45
+ name: string;
46
+ cwd: string;
47
+ matches: Match[];
48
+ truncated: boolean;
49
+ }[];
50
+ total_matches: number;
51
+ total_files: number;
52
+ truncated: boolean;
53
+ elapsed_ms: number;
54
+ }
55
+ /** Exported for tests (not part of the package exports map). */
56
+ export declare function runGrep(pattern: string, paths: string[], opts: GrepOpts, context: HarneryProgramContext | undefined): Promise<GrepResult>;
57
+ type DecodeMode = "content" | "count" | "filesOnly" | "plainLines";
58
+ /**
59
+ * Streaming decoder for NUL-framed engine output. Exported for direct
60
+ * chunk-boundary tests. Record shapes (both engines, pinned by the parity
61
+ * suite):
62
+ * content: <path>NUL<line>:<text>LF
63
+ * count: <path>NUL<count>LF
64
+ * filesOnly: <path>NUL (NUL is the record terminator)
65
+ * plainLines: <path>LF (files mode: rg --files / find)
66
+ *
67
+ * Operates on Buffers so a chunk boundary can fall anywhere — including
68
+ * inside a multi-byte UTF-8 sequence — without corrupting a record: bytes
69
+ * are only decoded to strings once a full record is framed.
70
+ */
71
+ export declare class NulDecoder {
72
+ private readonly mode;
73
+ private buffer;
74
+ constructor(mode: DecodeMode);
75
+ push(chunk: Buffer): Omit<Match, "repo" | "kind">[];
76
+ /** Flush a trailing unterminated record (engine killed mid-write, or no final LF). */
77
+ flush(): Omit<Match, "repo" | "kind">[];
78
+ private decodeRecord;
79
+ }
80
+ export {};
4
81
  //# sourceMappingURL=grep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA4E1E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAoCN"}
1
+ {"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"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"}