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.
- package/README.md +2 -1
- package/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/callers.d.ts.map +1 -1
- package/dist/commands/callers.js +69 -17
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +84 -2
- package/dist/commands/grep.d.ts +77 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +598 -109
- package/dist/commands/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +89 -0
- package/dist/core/agents/cli.js +10 -3
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +26 -1
- package/dist/core/agents/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +13 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -4
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +38 -0
- package/dist/core/hooks/cli.js +29 -3
- package/dist/core/hooks/events/schema.d.ts +4 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +1 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- package/dist/core/workflow/billing.d.ts +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +102 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +340 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +30 -0
- package/dist/core/workflow/spawn-claude.d.ts +22 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +82 -0
- package/dist/core/workflow/spawn-codex.d.ts +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +66 -0
- package/dist/core/workflow/spawn-cursor.d.ts +26 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +72 -0
- package/dist/core/workflow/types.d.ts +145 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- package/dist/lib/tools/ripgrep.d.ts +61 -0
- package/dist/lib/tools/ripgrep.d.ts.map +1 -0
- package/dist/lib/tools/ripgrep.js +217 -0
- package/package.json +1 -1
- package/src/commander.ts +11 -0
- package/src/commands/callers.ts +90 -26
- package/src/commands/doctor.ts +95 -2
- package/src/commands/grep.ts +737 -113
- package/src/commands/init.ts +138 -17
- package/src/commands/workflow.ts +132 -0
- package/src/core/agents/cli.ts +11 -4
- package/src/core/agents/rules/claim-conflict.ts +26 -1
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/state/heartbeat-projector.ts +13 -1
- package/src/core/agents/state/heartbeat-writer.ts +30 -5
- package/src/core/config.ts +47 -0
- package/src/core/hooks/cli.ts +30 -3
- package/src/core/hooks/events/schema.ts +4 -0
- package/src/core/hooks/harness/events.ts +8 -0
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- package/src/core/workflow/billing.ts +146 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +394 -0
- package/src/core/workflow/harnesses.ts +38 -0
- package/src/core/workflow/spawn-claude.ts +99 -0
- package/src/core/workflow/spawn-codex.ts +74 -0
- package/src/core/workflow/spawn-cursor.ts +89 -0
- package/src/core/workflow/types.ts +153 -0
- package/src/core/workflow/validate.ts +75 -0
- package/src/lib/tools/ripgrep.ts +244 -0
package/src/commands/callers.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
4
|
+
import { resolveSearchEngine, type SearchEngine } from "../lib/tools/ripgrep.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `harn callers <symbol>`: find references to a symbol across the monorepo with
|
|
7
|
-
* kind classification (call / import / type / decl / ref).
|
|
8
|
-
* grep
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* kind classification (call / import / type / decl / ref). Whole-word search
|
|
9
|
+
* (ripgrep when available, GNU grep fallback — same engine + provisioning path
|
|
10
|
+
* as `harn grep`, honoring HARNERY_GREP_ENGINE) with post-filtering:
|
|
11
|
+
* declarations + single-line comments are filtered out by default, and lines
|
|
12
|
+
* starting with " * " (multi-line comment continuation) are dropped. Inherent
|
|
13
|
+
* text-based-search limitations: multi-line string literals and block-comment
|
|
14
|
+
* interiors aren't excluded. Repos are searched in parallel.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
interface CallersOpts {
|
|
@@ -116,21 +119,31 @@ async function runCallers(
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
const started = Date.now();
|
|
122
|
+
const { engine, rgBin } = await resolveSearchEngine("callers");
|
|
119
123
|
const repos = resolveRepos(opts, context);
|
|
120
124
|
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : 500;
|
|
121
125
|
|
|
126
|
+
// Search every repo concurrently, each capped at the global limit, then
|
|
127
|
+
// apply the global budget in repo order so truncation stays deterministic.
|
|
128
|
+
// In --all-repos mode the parent scan prunes submodule dirs (each submodule
|
|
129
|
+
// is scanned on its own), so a match is attributed to exactly one repo.
|
|
130
|
+
const perRepo = await Promise.all(
|
|
131
|
+
repos.map((repo) => {
|
|
132
|
+
const pruneDirs = opts.allRepos && repo.name === "parent" ? (context?.submodules ?? []) : [];
|
|
133
|
+
return searchInRepo(symbol, opts, repo, limit, engine, rgBin, pruneDirs);
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
122
137
|
const allCallers: Caller[] = [];
|
|
123
138
|
let truncated = false;
|
|
124
|
-
|
|
125
|
-
for (
|
|
126
|
-
|
|
127
|
-
truncated = true;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
const remaining = limit - allCallers.length;
|
|
131
|
-
const found = await grepInRepo(symbol, opts, repo, remaining);
|
|
139
|
+
let budget = limit;
|
|
140
|
+
for (let i = 0; i < repos.length; i++) {
|
|
141
|
+
const found = perRepo[i] ?? { callers: [], truncated: false };
|
|
132
142
|
if (found.truncated) truncated = true;
|
|
133
|
-
|
|
143
|
+
const take = Math.min(found.callers.length, Math.max(0, budget));
|
|
144
|
+
if (found.callers.length > take) truncated = true;
|
|
145
|
+
allCallers.push(...found.callers.slice(0, take));
|
|
146
|
+
budget -= take;
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
const by_kind: Record<string, number> = {};
|
|
@@ -184,24 +197,72 @@ function resolveRepos(
|
|
|
184
197
|
return [{ name: "cwd", cwd: process.cwd() }];
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
function
|
|
188
|
-
symbol: string,
|
|
189
|
-
opts: CallersOpts,
|
|
190
|
-
repo: { name: string; cwd: string },
|
|
191
|
-
limit: number,
|
|
192
|
-
): Promise<{ callers: Caller[]; truncated: boolean }> {
|
|
193
|
-
const args: string[] = ["-rnw", "--color=never", "-I", "-E"];
|
|
194
|
-
for (const d of DEFAULT_EXCLUDE_DIRS) args.push(`--exclude-dir=${d}`);
|
|
195
|
-
for (const f of DEFAULT_EXCLUDE_FILES) args.push(`--exclude=${f}`);
|
|
200
|
+
function resolveLangGlobs(opts: CallersOpts): string[] | undefined {
|
|
196
201
|
const langGlobs = opts.lang ? LANG_GLOBS[opts.lang] : undefined;
|
|
197
202
|
if (opts.lang && !langGlobs) {
|
|
198
203
|
throw new Error(`unknown --lang "${opts.lang}". Valid: ${Object.keys(LANG_GLOBS).join(", ")}`);
|
|
199
204
|
}
|
|
205
|
+
return langGlobs;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildCallersGrepArgs(
|
|
209
|
+
symbol: string,
|
|
210
|
+
opts: CallersOpts,
|
|
211
|
+
extraExcludeDirs: readonly string[],
|
|
212
|
+
): string[] {
|
|
213
|
+
const args: string[] = ["-rnw", "-H", "--color=never", "-I", "-E"];
|
|
214
|
+
for (const d of DEFAULT_EXCLUDE_DIRS) args.push(`--exclude-dir=${d}`);
|
|
215
|
+
for (const f of DEFAULT_EXCLUDE_FILES) args.push(`--exclude=${f}`);
|
|
216
|
+
for (const d of extraExcludeDirs) args.push(`--exclude-dir=${d}`);
|
|
217
|
+
const langGlobs = resolveLangGlobs(opts);
|
|
200
218
|
if (langGlobs) for (const g of langGlobs) args.push(`--include=${g}`);
|
|
201
219
|
args.push("--", symbol, ".");
|
|
220
|
+
return args;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildCallersRgArgs(
|
|
224
|
+
symbol: string,
|
|
225
|
+
opts: CallersOpts,
|
|
226
|
+
extraExcludeDirs: readonly string[],
|
|
227
|
+
): string[] {
|
|
228
|
+
// --hidden --no-ignore --no-config: match grep semantics (dotdirs searched,
|
|
229
|
+
// .gitignore ignored, user rg config can't skew results). -w = whole word.
|
|
230
|
+
// Lang globs (positives) go before dir excludes (negatives) since rg globs
|
|
231
|
+
// are last-match-wins.
|
|
232
|
+
const args: string[] = [
|
|
233
|
+
"-nw",
|
|
234
|
+
"--no-heading",
|
|
235
|
+
"--with-filename",
|
|
236
|
+
"--color=never",
|
|
237
|
+
"--no-config",
|
|
238
|
+
"--hidden",
|
|
239
|
+
"--no-ignore",
|
|
240
|
+
];
|
|
241
|
+
const langGlobs = resolveLangGlobs(opts);
|
|
242
|
+
if (langGlobs) for (const g of langGlobs) args.push(`--glob=${g}`);
|
|
243
|
+
for (const d of DEFAULT_EXCLUDE_DIRS) args.push(`--glob=!${d}`);
|
|
244
|
+
for (const f of DEFAULT_EXCLUDE_FILES) args.push(`--glob=!${f}`);
|
|
245
|
+
for (const d of extraExcludeDirs) args.push(`--glob=!${d}`);
|
|
246
|
+
args.push("--", symbol, ".");
|
|
247
|
+
return args;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function searchInRepo(
|
|
251
|
+
symbol: string,
|
|
252
|
+
opts: CallersOpts,
|
|
253
|
+
repo: { name: string; cwd: string },
|
|
254
|
+
limit: number,
|
|
255
|
+
engine: SearchEngine,
|
|
256
|
+
rgBin: string,
|
|
257
|
+
extraExcludeDirs: readonly string[],
|
|
258
|
+
): Promise<{ callers: Caller[]; truncated: boolean }> {
|
|
259
|
+
const [bin, args] =
|
|
260
|
+
engine === "rg"
|
|
261
|
+
? ([rgBin, buildCallersRgArgs(symbol, opts, extraExcludeDirs)] as const)
|
|
262
|
+
: (["grep", buildCallersGrepArgs(symbol, opts, extraExcludeDirs)] as const);
|
|
202
263
|
|
|
203
264
|
return new Promise((resolveP, reject) => {
|
|
204
|
-
const proc = spawn(
|
|
265
|
+
const proc = spawn(bin, args, { cwd: repo.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
205
266
|
const callers: Caller[] = [];
|
|
206
267
|
let buffer = "";
|
|
207
268
|
let stderr = "";
|
|
@@ -237,8 +298,11 @@ function grepInRepo(
|
|
|
237
298
|
const parsed = parseCallerLine(buffer, symbol, opts, repo.name);
|
|
238
299
|
if (parsed) callers.push(parsed);
|
|
239
300
|
}
|
|
240
|
-
|
|
241
|
-
|
|
301
|
+
// Exit 1 = no matches (both engines): normal. Exit 2 with nothing
|
|
302
|
+
// collected = a real failure; with matches, a partial walk error we
|
|
303
|
+
// tolerate (surface what was found).
|
|
304
|
+
if (code !== null && code !== 0 && code !== 1 && !truncated && callers.length === 0) {
|
|
305
|
+
reject(new Error(`${bin} exited ${code}: ${stderr.trim() || "(no stderr)"}`));
|
|
242
306
|
return;
|
|
243
307
|
}
|
|
244
308
|
resolveP({ callers, truncated });
|
package/src/commands/doctor.ts
CHANGED
|
@@ -17,8 +17,16 @@ import os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import type { Command } from "commander";
|
|
19
19
|
import type { EmitContext } from "../commander.ts";
|
|
20
|
-
import { resolveBinName } from "../core/config.ts";
|
|
20
|
+
import { resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
|
|
21
21
|
import { loadHarnessWiring } from "../core/hooks/harness/wiring.ts";
|
|
22
|
+
import { probeBilling } from "../core/workflow/billing.ts";
|
|
23
|
+
import {
|
|
24
|
+
HARNESS_BINARIES,
|
|
25
|
+
HARNESS_INSTALL_HINTS,
|
|
26
|
+
HARNESS_LOGIN_HINTS,
|
|
27
|
+
} from "../core/workflow/harnesses.ts";
|
|
28
|
+
import type { HarnessName } from "../core/workflow/types.ts";
|
|
29
|
+
import { findRg, installRg, managedRgPath, rgInstallSupported } from "../lib/tools/ripgrep.ts";
|
|
22
30
|
|
|
23
31
|
type Severity = "ok" | "warn" | "fail";
|
|
24
32
|
|
|
@@ -31,6 +39,7 @@ interface Check {
|
|
|
31
39
|
|
|
32
40
|
interface CheckOpts {
|
|
33
41
|
json?: boolean;
|
|
42
|
+
fix?: boolean;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export function registerDoctorCommand(program: Command, emit: EmitContext): void {
|
|
@@ -41,7 +50,18 @@ export function registerDoctorCommand(program: Command, emit: EmitContext): void
|
|
|
41
50
|
"unless a required dep (Node, git) is missing.",
|
|
42
51
|
)
|
|
43
52
|
.option("--json", "Machine-readable JSON output")
|
|
44
|
-
.
|
|
53
|
+
.option(
|
|
54
|
+
"--fix",
|
|
55
|
+
"Install missing managed tools (currently: ripgrep, pinned + checksum-verified)",
|
|
56
|
+
)
|
|
57
|
+
.action(async (opts: CheckOpts) => {
|
|
58
|
+
if (opts.fix && !findRg() && rgInstallSupported()) {
|
|
59
|
+
try {
|
|
60
|
+
await installRg((line) => emit.text(`${line}\n`));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
emit.text(`ripgrep install failed: ${(err as Error).message}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
45
65
|
const checks = runChecks();
|
|
46
66
|
const requiredFailed = checks.some((c) => c.severity === "fail");
|
|
47
67
|
|
|
@@ -80,8 +100,12 @@ export function runChecks(): Check[] {
|
|
|
80
100
|
checkNode(),
|
|
81
101
|
checkGit(),
|
|
82
102
|
checkBun(),
|
|
103
|
+
checkRipgrep(),
|
|
83
104
|
checkHarneryDir(),
|
|
84
105
|
checkHarnessHooks(),
|
|
106
|
+
checkWorkflowHarness("claude-code"),
|
|
107
|
+
checkWorkflowHarness("codex"),
|
|
108
|
+
checkWorkflowHarness("cursor"),
|
|
85
109
|
checkRestic(),
|
|
86
110
|
checkRclone(),
|
|
87
111
|
checkPlaywright(),
|
|
@@ -89,6 +113,40 @@ export function runChecks(): Check[] {
|
|
|
89
113
|
];
|
|
90
114
|
}
|
|
91
115
|
|
|
116
|
+
function checkRipgrep(): Check {
|
|
117
|
+
const found = findRg();
|
|
118
|
+
if (found) {
|
|
119
|
+
const r = whichVersion(found);
|
|
120
|
+
const managed = found === managedRgPath() ? ", managed" : "";
|
|
121
|
+
return {
|
|
122
|
+
name: "ripgrep",
|
|
123
|
+
severity: "ok",
|
|
124
|
+
detail: `${r.out.replace(/^ripgrep\s*/, "")} (${found === "rg" ? "PATH" : found}${managed})`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (!rgInstallSupported()) {
|
|
128
|
+
return {
|
|
129
|
+
name: "ripgrep",
|
|
130
|
+
severity: "warn",
|
|
131
|
+
detail: "missing (grep fallback; no pinned artifact for this OS/arch)",
|
|
132
|
+
hint: "https://github.com/BurntSushi/ripgrep#installation",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (ripgrepAutoInstall()) {
|
|
136
|
+
return {
|
|
137
|
+
name: "ripgrep",
|
|
138
|
+
severity: "warn",
|
|
139
|
+
detail: "missing (autoInstall on: will self-provision on first grep)",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
name: "ripgrep",
|
|
144
|
+
severity: "warn",
|
|
145
|
+
detail: "missing (grep fallback works, just slower)",
|
|
146
|
+
hint: `${resolveBinName()} doctor --fix (pinned + checksum-verified, installs to the harnery tools dir)`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
92
150
|
function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolean; out: string } {
|
|
93
151
|
const r = spawnSync(bin, args, { encoding: "utf-8" });
|
|
94
152
|
if (r.status !== 0) return { ok: false, out: "" };
|
|
@@ -96,6 +154,41 @@ function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolea
|
|
|
96
154
|
return { ok: true, out };
|
|
97
155
|
}
|
|
98
156
|
|
|
157
|
+
/**
|
|
158
|
+
* One workflow spawn target: is the harness CLI installed, and how will its
|
|
159
|
+
* headless children bill (subscription login vs API key — see billing.ts)?
|
|
160
|
+
* Missing is a warn, not a fail: workflows degrade to the harnesses you have.
|
|
161
|
+
*/
|
|
162
|
+
function checkWorkflowHarness(harness: HarnessName): Check {
|
|
163
|
+
const bin = HARNESS_BINARIES[harness];
|
|
164
|
+
const name = `workflow:${harness}`;
|
|
165
|
+
const r = whichVersion(bin);
|
|
166
|
+
if (!r.ok) {
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
severity: "warn",
|
|
170
|
+
detail: `${bin} missing (workflow --harness ${harness} unavailable)`,
|
|
171
|
+
hint: `${HARNESS_INSTALL_HINTS[harness]} then: ${HARNESS_LOGIN_HINTS[harness]}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const probe = probeBilling(harness);
|
|
175
|
+
if (probe.login === "absent" && !probe.apiKeyPresent) {
|
|
176
|
+
return {
|
|
177
|
+
name,
|
|
178
|
+
severity: "warn",
|
|
179
|
+
detail: `${r.out} — installed, but no stored login or API key detected`,
|
|
180
|
+
hint: HARNESS_LOGIN_HINTS[harness],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const billing =
|
|
184
|
+
probe.mode === "subscription"
|
|
185
|
+
? probe.login === "present"
|
|
186
|
+
? "billing: subscription"
|
|
187
|
+
: "billing: subscription (login unverifiable, CLI is the authority)"
|
|
188
|
+
: `billing: ${probe.mode} (${probe.apiKeySource})`;
|
|
189
|
+
return { name, severity: "ok", detail: `${r.out} — ${billing}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
99
192
|
function checkNode(): Check {
|
|
100
193
|
const v = process.versions.node;
|
|
101
194
|
const major = Number.parseInt(v.split(".")[0], 10);
|