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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cursor spawn adapter: runs one subagent as a headless `cursor-agent -p`
|
|
3
|
+
* subprocess with `--output-format json`.
|
|
4
|
+
*
|
|
5
|
+
* Contract notes (LIVE-VERIFIED 2026-07-17 against cursor-agent
|
|
6
|
+
* 2026.07.16-899851b: schema-gated triage + text stages round-trip via
|
|
7
|
+
* `--harness cursor`, session_id parses from the envelope):
|
|
8
|
+
* - `cursor-agent -p "<prompt>" --output-format json` prints a single result
|
|
9
|
+
* envelope modeled on Claude Code's (`{type: "result", is_error, result,
|
|
10
|
+
* session_id, …}`).
|
|
11
|
+
* - `--trust` is required: headless runs refuse untrusted workspaces (exit 1,
|
|
12
|
+
* "Workspace Trust Required") — see the argv comment below.
|
|
13
|
+
* - Envelope drift guard: when stdout doesn't parse as JSON but the process
|
|
14
|
+
* exited 0, the raw stdout is returned as the reply text.
|
|
15
|
+
* - No per-run cost surface → undefined. No max-turns equivalent → `maxTurns`
|
|
16
|
+
* accepted and ignored (documented in the CLI docs page).
|
|
17
|
+
*/
|
|
18
|
+
import { exec } from "../../lib/exec.js";
|
|
19
|
+
import { buildChildEnv } from "./child-env.js";
|
|
20
|
+
import { notFoundError } from "./harnesses.js";
|
|
21
|
+
/** Exported for unit tests (no live binary to test against). */
|
|
22
|
+
export function parseCursorOutput(stdout) {
|
|
23
|
+
try {
|
|
24
|
+
const envelope = JSON.parse(stdout);
|
|
25
|
+
return {
|
|
26
|
+
text: String(envelope.result ?? ""),
|
|
27
|
+
sessionId: envelope.session_id,
|
|
28
|
+
isError: Boolean(envelope.is_error),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { text: stdout, isError: false };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const cursorSpawner = async (req) => {
|
|
36
|
+
const t0 = Date.now();
|
|
37
|
+
// --trust: headless cursor-agent refuses untrusted workspaces (exit 1,
|
|
38
|
+
// "Workspace Trust Required"). Workflow children run in the engine's cwd
|
|
39
|
+
// deliberately and may edit files — the same posture as the codex adapter's
|
|
40
|
+
// `--sandbox workspace-write` — so trusting that directory is implied.
|
|
41
|
+
const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust"];
|
|
42
|
+
if (req.model)
|
|
43
|
+
argv.push("--model", req.model);
|
|
44
|
+
const r = await exec(argv, {
|
|
45
|
+
cwd: req.cwd,
|
|
46
|
+
env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
|
|
47
|
+
timeout: req.timeoutMs,
|
|
48
|
+
});
|
|
49
|
+
const durationMs = Date.now() - t0;
|
|
50
|
+
if (r.exitCode === 127) {
|
|
51
|
+
return { ok: false, text: "", durationMs, error: notFoundError("cursor") };
|
|
52
|
+
}
|
|
53
|
+
if (r.exitCode !== 0) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
text: "",
|
|
57
|
+
durationMs,
|
|
58
|
+
error: `cursor-agent exited ${r.exitCode}: ${(r.stderr || r.stdout).slice(0, 500)}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseCursorOutput(r.stdout);
|
|
62
|
+
if (parsed.isError) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
text: parsed.text,
|
|
66
|
+
sessionId: parsed.sessionId,
|
|
67
|
+
durationMs,
|
|
68
|
+
error: `cursor-agent reported is_error: ${parsed.text.slice(0, 300)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, text: parsed.text, sessionId: parsed.sessionId, durationMs };
|
|
72
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow engine contracts. A workflow is a small throwaway JS script with
|
|
3
|
+
* bounded, schema-gated stages that fan work out to headless harness-CLI
|
|
4
|
+
* subagents; the SCRIPT (deterministic code), not any model, decides routing
|
|
5
|
+
* between stages, and the run always terminates when the script returns.
|
|
6
|
+
*
|
|
7
|
+
* Design record: decision 0015 (portable coordination-aware workflows).
|
|
8
|
+
*/
|
|
9
|
+
import type { BillingMode, BillingProber } from "./billing.js";
|
|
10
|
+
/** JSON-schema *subset* accepted by stage gates (see validate.ts). */
|
|
11
|
+
export interface StageSchema {
|
|
12
|
+
type: "object" | "array" | "string" | "number" | "boolean";
|
|
13
|
+
/** type=object */
|
|
14
|
+
properties?: Record<string, StageSchema>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
/** type=array */
|
|
17
|
+
items?: StageSchema;
|
|
18
|
+
/** any type: closed value set (compared with ===) */
|
|
19
|
+
enum?: Array<string | number | boolean>;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentOpts {
|
|
22
|
+
/** Stage gate: when present, the agent's reply must strict-parse as JSON and
|
|
23
|
+
* validate; the engine retries with the validation error appended, up to
|
|
24
|
+
* `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
|
|
25
|
+
schema?: StageSchema;
|
|
26
|
+
/** Model slug passed through to the harness CLI (default: the CLI's default). */
|
|
27
|
+
model?: string;
|
|
28
|
+
/** Attempt ceiling for the schema-retry loop (default 2). */
|
|
29
|
+
maxAttempts?: number;
|
|
30
|
+
/** Subprocess timeout ms (default 300_000). */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/** Harness-turn ceiling for the child (default 25; use 1 for pure
|
|
33
|
+
* classification stages — cheaper and faster). */
|
|
34
|
+
maxTurns?: number;
|
|
35
|
+
/** Display label in the journal (default: prompt head). */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Which harness runs this agent (default: the run's default harness).
|
|
38
|
+
* Mixed-harness workflows are legal: triage on one CLI, deep work on
|
|
39
|
+
* another. */
|
|
40
|
+
harness?: HarnessName;
|
|
41
|
+
}
|
|
42
|
+
export type HarnessName = "claude-code" | "codex" | "cursor";
|
|
43
|
+
/** What a spawn adapter returns for one subagent run. */
|
|
44
|
+
export interface SpawnResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
/** The model's final reply text (envelope-unwrapped). */
|
|
47
|
+
text: string;
|
|
48
|
+
/** Child harness session id when the envelope carries one. */
|
|
49
|
+
sessionId?: string;
|
|
50
|
+
costUsd?: number;
|
|
51
|
+
durationMs: number;
|
|
52
|
+
/** Populated when ok=false. */
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface SpawnRequest {
|
|
56
|
+
prompt: string;
|
|
57
|
+
model?: string;
|
|
58
|
+
timeoutMs: number;
|
|
59
|
+
maxTurns: number;
|
|
60
|
+
cwd: string;
|
|
61
|
+
/** Run id, stamped into the child env (HARNERY_WORKFLOW_RUN_ID) so the
|
|
62
|
+
* coord layer can associate child sessions with their workflow run. */
|
|
63
|
+
runId?: string;
|
|
64
|
+
/** Scrub all API-key vars from the child env so it can only authenticate
|
|
65
|
+
* via its stored (subscription) login. See billing.ts. */
|
|
66
|
+
subscriptionOnly?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** One headless-subagent runner. The engine is adapter-agnostic; claude-code
|
|
69
|
+
* ships first, codex/cursor land behind the same signature (plan Phase 4). */
|
|
70
|
+
export type Spawner = (req: SpawnRequest) => Promise<SpawnResult>;
|
|
71
|
+
/** The API surface injected into a workflow script's default export. Explicit
|
|
72
|
+
* injection (no ambient globals): keeps scripts portable and unit-testable. */
|
|
73
|
+
export interface WorkflowContext {
|
|
74
|
+
/** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
|
|
75
|
+
agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
|
|
76
|
+
/** Run thunks with bounded concurrency; a rejected thunk resolves to null. */
|
|
77
|
+
parallel: <T>(thunks: Array<() => Promise<T>>) => Promise<Array<T | null>>;
|
|
78
|
+
/** Declare the current stage (journal + progress grouping). */
|
|
79
|
+
stage: (title: string) => void;
|
|
80
|
+
/** Narrate progress (stderr + journal). */
|
|
81
|
+
log: (message: string) => void;
|
|
82
|
+
}
|
|
83
|
+
export interface WorkflowMeta {
|
|
84
|
+
name: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
}
|
|
87
|
+
/** Loaded script shape: `export const meta` + `export default async (ctx) => …`. */
|
|
88
|
+
export interface WorkflowModule {
|
|
89
|
+
meta?: WorkflowMeta;
|
|
90
|
+
default: (ctx: WorkflowContext) => Promise<unknown>;
|
|
91
|
+
}
|
|
92
|
+
export interface EngineOpts {
|
|
93
|
+
/** Repo root whose .harnery/ receives the run journal. */
|
|
94
|
+
coordRoot: string;
|
|
95
|
+
/** Spawner registry keyed by harness. A single-harness caller registers one
|
|
96
|
+
* entry and names it in `defaultHarness`. */
|
|
97
|
+
spawners: Partial<Record<HarnessName, Spawner>>;
|
|
98
|
+
/** Harness used when an agent() call doesn't name one (default "claude-code"). */
|
|
99
|
+
defaultHarness?: HarnessName;
|
|
100
|
+
/** Resume: run id of a prior run whose journal supplies cached results.
|
|
101
|
+
* agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
|
|
102
|
+
* completed prior agent return the journaled result without spawning. */
|
|
103
|
+
resumeFrom?: string;
|
|
104
|
+
/** Total-agent ceiling for the run (default 50): the runaway backstop. */
|
|
105
|
+
maxAgents?: number;
|
|
106
|
+
/** Concurrent-subagent cap for parallel() (default 4). */
|
|
107
|
+
concurrency?: number;
|
|
108
|
+
/** Working directory children spawn in (default: coordRoot). */
|
|
109
|
+
cwd?: string;
|
|
110
|
+
/** Progress sink (default: process.stderr). */
|
|
111
|
+
onLog?: (line: string) => void;
|
|
112
|
+
/** Guarantee subscription billing: API-key vars are scrubbed from every
|
|
113
|
+
* child env, and a harness whose stored login is provably absent fails
|
|
114
|
+
* loud before spawning. */
|
|
115
|
+
subscriptionOnly?: boolean;
|
|
116
|
+
/** Permit the api-key-override billing state (an exported API key silently
|
|
117
|
+
* shadowing a stored subscription login), which the engine otherwise
|
|
118
|
+
* refuses. Deliberate key-only hosts don't need this — only the
|
|
119
|
+
* both-present case does. */
|
|
120
|
+
allowApiBilling?: boolean;
|
|
121
|
+
/** Billing-probe override for tests (default: the real probeBilling). */
|
|
122
|
+
probeBilling?: BillingProber;
|
|
123
|
+
}
|
|
124
|
+
export interface RunReport {
|
|
125
|
+
runId: string;
|
|
126
|
+
name: string;
|
|
127
|
+
/** What the script's default export returned. */
|
|
128
|
+
result: unknown;
|
|
129
|
+
agentsSpawned: number;
|
|
130
|
+
/** agent() calls satisfied from the resumeFrom journal without spawning. */
|
|
131
|
+
agentsCached: number;
|
|
132
|
+
costUsd: number;
|
|
133
|
+
durationMs: number;
|
|
134
|
+
journalPath: string;
|
|
135
|
+
/** Estimated tokens of repo instructions (CLAUDE.md/AGENTS.md at the child
|
|
136
|
+
* cwd) that EVERY child cache-writes on spawn — the fixed per-child context
|
|
137
|
+
* overhead a fan-out multiplies. bytes/4 heuristic; 0 when no such file. */
|
|
138
|
+
contextTokensPerChildEstimate: number;
|
|
139
|
+
/** Billing mode per harness actually used this run (probed on first use). */
|
|
140
|
+
billing: Array<{
|
|
141
|
+
harness: HarnessName;
|
|
142
|
+
mode: BillingMode;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow engine contracts. A workflow is a small throwaway JS script with
|
|
3
|
+
* bounded, schema-gated stages that fan work out to headless harness-CLI
|
|
4
|
+
* subagents; the SCRIPT (deterministic code), not any model, decides routing
|
|
5
|
+
* between stages, and the run always terminates when the script returns.
|
|
6
|
+
*
|
|
7
|
+
* Design record: decision 0015 (portable coordination-aware workflows).
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal validator for the StageSchema JSON-schema subset. Deliberately tiny:
|
|
3
|
+
* a full JSON Schema implementation would pull a dependency (ajv) for
|
|
4
|
+
* validation depth workflow gates don't need. Supported: type, properties,
|
|
5
|
+
* required, items, enum. Returns a list of human-readable problems (empty =
|
|
6
|
+
* valid) so the engine can feed failures back into the retry prompt verbatim.
|
|
7
|
+
*/
|
|
8
|
+
import type { StageSchema } from "./types.js";
|
|
9
|
+
export declare function validateAgainstSchema(value: unknown, schema: StageSchema, path?: string): string[];
|
|
10
|
+
/** Strip accidental markdown code fences, then strict-parse JSON. */
|
|
11
|
+
export declare function parseStageOutput(text: string): {
|
|
12
|
+
value?: unknown;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CA8C/F;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAUlF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal validator for the StageSchema JSON-schema subset. Deliberately tiny:
|
|
3
|
+
* a full JSON Schema implementation would pull a dependency (ajv) for
|
|
4
|
+
* validation depth workflow gates don't need. Supported: type, properties,
|
|
5
|
+
* required, items, enum. Returns a list of human-readable problems (empty =
|
|
6
|
+
* valid) so the engine can feed failures back into the retry prompt verbatim.
|
|
7
|
+
*/
|
|
8
|
+
export function validateAgainstSchema(value, schema, path = "$") {
|
|
9
|
+
const problems = [];
|
|
10
|
+
if (schema.enum) {
|
|
11
|
+
if (!schema.enum.some((v) => v === value)) {
|
|
12
|
+
problems.push(`${path}: expected one of ${JSON.stringify(schema.enum)}, got ${short(value)}`);
|
|
13
|
+
}
|
|
14
|
+
return problems; // enum is exhaustive; type check is implied by membership
|
|
15
|
+
}
|
|
16
|
+
switch (schema.type) {
|
|
17
|
+
case "object": {
|
|
18
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
19
|
+
return [`${path}: expected object, got ${short(value)}`];
|
|
20
|
+
}
|
|
21
|
+
const obj = value;
|
|
22
|
+
for (const key of schema.required ?? []) {
|
|
23
|
+
if (!(key in obj))
|
|
24
|
+
problems.push(`${path}.${key}: required property missing`);
|
|
25
|
+
}
|
|
26
|
+
for (const [key, sub] of Object.entries(schema.properties ?? {})) {
|
|
27
|
+
if (key in obj)
|
|
28
|
+
problems.push(...validateAgainstSchema(obj[key], sub, `${path}.${key}`));
|
|
29
|
+
}
|
|
30
|
+
return problems;
|
|
31
|
+
}
|
|
32
|
+
case "array": {
|
|
33
|
+
if (!Array.isArray(value))
|
|
34
|
+
return [`${path}: expected array, got ${short(value)}`];
|
|
35
|
+
if (schema.items) {
|
|
36
|
+
for (const [i, item] of value.entries()) {
|
|
37
|
+
problems.push(...validateAgainstSchema(item, schema.items, `${path}[${i}]`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return problems;
|
|
41
|
+
}
|
|
42
|
+
case "string":
|
|
43
|
+
case "number":
|
|
44
|
+
case "boolean": {
|
|
45
|
+
if (typeof value !== schema.type) {
|
|
46
|
+
problems.push(`${path}: expected ${schema.type}, got ${short(value)}`);
|
|
47
|
+
}
|
|
48
|
+
return problems;
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return [`${path}: unsupported schema type ${String(schema.type)}`];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Strip accidental markdown code fences, then strict-parse JSON. */
|
|
55
|
+
export function parseStageOutput(text) {
|
|
56
|
+
const stripped = text
|
|
57
|
+
.trim()
|
|
58
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
59
|
+
.replace(/\s*```$/, "");
|
|
60
|
+
try {
|
|
61
|
+
return { value: JSON.parse(stripped) };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return { error: `not valid JSON: ${err.message}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function short(value) {
|
|
68
|
+
const s = JSON.stringify(value);
|
|
69
|
+
return s === undefined ? String(value) : s.length > 60 ? `${s.slice(0, 60)}…` : s;
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed ripgrep provisioning for `grep --files` / content search.
|
|
3
|
+
*
|
|
4
|
+
* harnery can install a pinned, checksum-verified ripgrep into its own tools
|
|
5
|
+
* directory (`$XDG_DATA_HOME/harnery/tools`, default `~/.local/share/harnery/
|
|
6
|
+
* tools`) so `harn grep` gets the fast engine even on machines where nobody
|
|
7
|
+
* ever installed rg. The download is version-pinned and every artifact's
|
|
8
|
+
* sha256 is baked in below (cross-checked against the official release
|
|
9
|
+
* checksums), so the install is reproducible, never "whatever is latest."
|
|
10
|
+
*
|
|
11
|
+
* Consent model: the installer only runs automatically when the host project
|
|
12
|
+
* opts in via `.harnery/config.jsonc` `{ "tools": { "ripgrep": { "autoInstall":
|
|
13
|
+
* true } } }` — a repo commits that once and every clone self-heals. Without
|
|
14
|
+
* the opt-in, a missing rg produces a rate-limited stderr hint naming
|
|
15
|
+
* `harn doctor --fix` (explicit install). Environments that forbid downloads
|
|
16
|
+
* lose nothing: every failure path falls back to GNU grep.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RG_VERSION = "14.1.1";
|
|
19
|
+
/** True when a pinned artifact exists for this OS/arch (Windows: not yet). */
|
|
20
|
+
export declare function rgInstallSupported(): boolean;
|
|
21
|
+
/** Machine-level harnery tools dir (NOT the per-project .harnery/ state dir). */
|
|
22
|
+
export declare function toolsDir(): string;
|
|
23
|
+
/** Path the managed rg binary lives at once installed. */
|
|
24
|
+
export declare function managedRgPath(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Locate a usable ripgrep. Precedence:
|
|
27
|
+
* 1. HARNERY_RG_PATH (explicit override)
|
|
28
|
+
* 2. the managed tools-dir install (works even when PATH doesn't carry it)
|
|
29
|
+
* 3. `rg` on PATH
|
|
30
|
+
* Returns the spawnable path/name, or null when none respond to --version.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findRg(): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Download + verify + install the pinned ripgrep into the tools dir.
|
|
35
|
+
* Atomic: extracts to a temp dir and renames the binary into place.
|
|
36
|
+
* Throws on unsupported platform, network failure, or checksum mismatch —
|
|
37
|
+
* callers are expected to fall back to grep and say why.
|
|
38
|
+
*/
|
|
39
|
+
export declare function installRg(log?: (line: string) => void): Promise<string>;
|
|
40
|
+
export type SearchEngine = "rg" | "grep";
|
|
41
|
+
/**
|
|
42
|
+
* Shared engine resolver for the ripgrep-backed search commands (`grep`,
|
|
43
|
+
* `callers`). Returns the engine plus the spawnable rg path.
|
|
44
|
+
*
|
|
45
|
+
* `HARNERY_GREP_ENGINE=rg|grep` forces one; otherwise findRg() probes
|
|
46
|
+
* HARNERY_RG_PATH → the managed tools-dir install → PATH. On a miss, the
|
|
47
|
+
* host's `.harnery/config.jsonc` `tools.ripgrep.autoInstall` consent triggers
|
|
48
|
+
* a pinned, checksum-verified install into the harnery tools dir (any failure
|
|
49
|
+
* falls back to grep with a stderr note); without consent, a rate-limited
|
|
50
|
+
* stderr hint (keyed per `hintKey`) names the explicit install command.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveSearchEngine(hintKey?: string): Promise<{
|
|
53
|
+
engine: SearchEngine;
|
|
54
|
+
rgBin: string;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Emit `line` to stderr at most once per 24h (stamp file in the tools dir).
|
|
58
|
+
* Keeps the "rg missing" nudge from nagging on every single grep invocation.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hintOncePerDay(line: string): void;
|
|
61
|
+
//# sourceMappingURL=ripgrep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripgrep.d.ts","sourceRoot":"","sources":["../../../src/lib/tools/ripgrep.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,UAAU,WAAW,CAAC;AA0BnC,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,IAAI,MAAM,CAIjC;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAWD;;;;;;GAMG;AACH,wBAAgB,MAAM,IAAI,MAAM,GAAG,IAAI,CAStC;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAoD7E;AAkBD,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,MAAM,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,SAAS,GACf,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BlD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAYjD"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveBinName, ripgrepAutoInstall } from "../../core/config.js";
|
|
8
|
+
import { coordEnv } from "../env.js";
|
|
9
|
+
/**
|
|
10
|
+
* Managed ripgrep provisioning for `grep --files` / content search.
|
|
11
|
+
*
|
|
12
|
+
* harnery can install a pinned, checksum-verified ripgrep into its own tools
|
|
13
|
+
* directory (`$XDG_DATA_HOME/harnery/tools`, default `~/.local/share/harnery/
|
|
14
|
+
* tools`) so `harn grep` gets the fast engine even on machines where nobody
|
|
15
|
+
* ever installed rg. The download is version-pinned and every artifact's
|
|
16
|
+
* sha256 is baked in below (cross-checked against the official release
|
|
17
|
+
* checksums), so the install is reproducible, never "whatever is latest."
|
|
18
|
+
*
|
|
19
|
+
* Consent model: the installer only runs automatically when the host project
|
|
20
|
+
* opts in via `.harnery/config.jsonc` `{ "tools": { "ripgrep": { "autoInstall":
|
|
21
|
+
* true } } }` — a repo commits that once and every clone self-heals. Without
|
|
22
|
+
* the opt-in, a missing rg produces a rate-limited stderr hint naming
|
|
23
|
+
* `harn doctor --fix` (explicit install). Environments that forbid downloads
|
|
24
|
+
* lose nothing: every failure path falls back to GNU grep.
|
|
25
|
+
*/
|
|
26
|
+
export const RG_VERSION = "14.1.1";
|
|
27
|
+
/** sha256 pins verified against the official ripgrep release .sha256 assets. */
|
|
28
|
+
const ARTIFACTS = {
|
|
29
|
+
"linux-x64": {
|
|
30
|
+
name: `ripgrep-${RG_VERSION}-x86_64-unknown-linux-musl.tar.gz`,
|
|
31
|
+
sha256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e",
|
|
32
|
+
},
|
|
33
|
+
"linux-arm64": {
|
|
34
|
+
name: `ripgrep-${RG_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
|
|
35
|
+
sha256: "c827481c4ff4ea10c9dc7a4022c8de5db34a5737cb74484d62eb94a95841ab2f",
|
|
36
|
+
},
|
|
37
|
+
"darwin-x64": {
|
|
38
|
+
name: `ripgrep-${RG_VERSION}-x86_64-apple-darwin.tar.gz`,
|
|
39
|
+
sha256: "fc87e78f7cb3fea12d69072e7ef3b21509754717b746368fd40d88963630e2b3",
|
|
40
|
+
},
|
|
41
|
+
"darwin-arm64": {
|
|
42
|
+
name: `ripgrep-${RG_VERSION}-aarch64-apple-darwin.tar.gz`,
|
|
43
|
+
sha256: "24ad76777745fbff131c8fbc466742b011f925bfa4fffa2ded6def23b5b937be",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
function platformKey() {
|
|
47
|
+
return `${process.platform}-${process.arch}`;
|
|
48
|
+
}
|
|
49
|
+
/** True when a pinned artifact exists for this OS/arch (Windows: not yet). */
|
|
50
|
+
export function rgInstallSupported() {
|
|
51
|
+
return platformKey() in ARTIFACTS;
|
|
52
|
+
}
|
|
53
|
+
/** Machine-level harnery tools dir (NOT the per-project .harnery/ state dir). */
|
|
54
|
+
export function toolsDir() {
|
|
55
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
56
|
+
const base = xdg && xdg.trim() !== "" ? xdg : join(homedir(), ".local", "share");
|
|
57
|
+
return join(base, "harnery", "tools");
|
|
58
|
+
}
|
|
59
|
+
/** Path the managed rg binary lives at once installed. */
|
|
60
|
+
export function managedRgPath() {
|
|
61
|
+
return join(toolsDir(), "rg");
|
|
62
|
+
}
|
|
63
|
+
function isExecutable(p) {
|
|
64
|
+
try {
|
|
65
|
+
const r = spawnSync(p, ["--version"], { stdio: "ignore" });
|
|
66
|
+
return r.status === 0;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Locate a usable ripgrep. Precedence:
|
|
74
|
+
* 1. HARNERY_RG_PATH (explicit override)
|
|
75
|
+
* 2. the managed tools-dir install (works even when PATH doesn't carry it)
|
|
76
|
+
* 3. `rg` on PATH
|
|
77
|
+
* Returns the spawnable path/name, or null when none respond to --version.
|
|
78
|
+
*/
|
|
79
|
+
export function findRg() {
|
|
80
|
+
const override = coordEnv("RG_PATH");
|
|
81
|
+
if (override && override.trim() !== "") {
|
|
82
|
+
return isExecutable(override) ? override : null;
|
|
83
|
+
}
|
|
84
|
+
const managed = managedRgPath();
|
|
85
|
+
if (existsSync(managed) && isExecutable(managed))
|
|
86
|
+
return managed;
|
|
87
|
+
if (isExecutable("rg"))
|
|
88
|
+
return "rg";
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Download + verify + install the pinned ripgrep into the tools dir.
|
|
93
|
+
* Atomic: extracts to a temp dir and renames the binary into place.
|
|
94
|
+
* Throws on unsupported platform, network failure, or checksum mismatch —
|
|
95
|
+
* callers are expected to fall back to grep and say why.
|
|
96
|
+
*/
|
|
97
|
+
export async function installRg(log) {
|
|
98
|
+
const key = platformKey();
|
|
99
|
+
const artifact = ARTIFACTS[key];
|
|
100
|
+
if (!artifact) {
|
|
101
|
+
throw new Error(`no pinned ripgrep artifact for ${key} — install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation)`);
|
|
102
|
+
}
|
|
103
|
+
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${artifact.name}`;
|
|
104
|
+
log?.(`downloading ${artifact.name} (pinned ${RG_VERSION})...`);
|
|
105
|
+
const res = await fetch(url);
|
|
106
|
+
if (!res.ok)
|
|
107
|
+
throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
108
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
109
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
110
|
+
if (digest !== artifact.sha256) {
|
|
111
|
+
throw new Error(`sha256 mismatch for ${artifact.name}: expected ${artifact.sha256}, got ${digest} — refusing to install`);
|
|
112
|
+
}
|
|
113
|
+
const work = join(tmpdir(), `harnery-rg-${process.pid}`);
|
|
114
|
+
rmSync(work, { recursive: true, force: true });
|
|
115
|
+
mkdirSync(work, { recursive: true });
|
|
116
|
+
try {
|
|
117
|
+
const tarball = join(work, artifact.name);
|
|
118
|
+
writeFileSync(tarball, bytes);
|
|
119
|
+
await extractTar(tarball, work);
|
|
120
|
+
const extractedRg = join(work, artifact.name.replace(/\.tar\.gz$/, ""), "rg");
|
|
121
|
+
const rgStat = await stat(extractedRg);
|
|
122
|
+
if (!rgStat.isFile())
|
|
123
|
+
throw new Error(`extracted archive missing rg binary at ${extractedRg}`);
|
|
124
|
+
mkdirSync(toolsDir(), { recursive: true });
|
|
125
|
+
const dest = managedRgPath();
|
|
126
|
+
// rename() can't cross filesystems (tmp is often tmpfs); copy + rename
|
|
127
|
+
// within the destination dir keeps the final placement atomic.
|
|
128
|
+
const staged = `${dest}.tmp-${process.pid}`;
|
|
129
|
+
writeFileSync(staged, await readFile(extractedRg));
|
|
130
|
+
chmodSync(staged, 0o755);
|
|
131
|
+
renameSync(staged, dest);
|
|
132
|
+
if (!isExecutable(dest)) {
|
|
133
|
+
rmSync(dest, { force: true });
|
|
134
|
+
throw new Error(`installed rg at ${dest} but it failed to execute`);
|
|
135
|
+
}
|
|
136
|
+
log?.(`installed ripgrep ${RG_VERSION} -> ${dest}`);
|
|
137
|
+
return dest;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
rmSync(work, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function extractTar(tarball, cwd) {
|
|
144
|
+
return new Promise((resolveP, reject) => {
|
|
145
|
+
const proc = spawn("tar", ["xzf", tarball], { cwd, stdio: ["ignore", "ignore", "pipe"] });
|
|
146
|
+
let stderr = "";
|
|
147
|
+
proc.stderr.setEncoding("utf8");
|
|
148
|
+
proc.stderr.on("data", (c) => {
|
|
149
|
+
stderr += c;
|
|
150
|
+
});
|
|
151
|
+
proc.on("error", reject);
|
|
152
|
+
proc.on("close", (code) => {
|
|
153
|
+
if (code === 0)
|
|
154
|
+
resolveP();
|
|
155
|
+
else
|
|
156
|
+
reject(new Error(`tar exited ${code}: ${stderr.trim()}`));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Shared engine resolver for the ripgrep-backed search commands (`grep`,
|
|
162
|
+
* `callers`). Returns the engine plus the spawnable rg path.
|
|
163
|
+
*
|
|
164
|
+
* `HARNERY_GREP_ENGINE=rg|grep` forces one; otherwise findRg() probes
|
|
165
|
+
* HARNERY_RG_PATH → the managed tools-dir install → PATH. On a miss, the
|
|
166
|
+
* host's `.harnery/config.jsonc` `tools.ripgrep.autoInstall` consent triggers
|
|
167
|
+
* a pinned, checksum-verified install into the harnery tools dir (any failure
|
|
168
|
+
* falls back to grep with a stderr note); without consent, a rate-limited
|
|
169
|
+
* stderr hint (keyed per `hintKey`) names the explicit install command.
|
|
170
|
+
*/
|
|
171
|
+
export async function resolveSearchEngine(hintKey = "grep") {
|
|
172
|
+
const forced = coordEnv("GREP_ENGINE");
|
|
173
|
+
if (forced === "grep")
|
|
174
|
+
return { engine: "grep", rgBin: "rg" };
|
|
175
|
+
const found = findRg();
|
|
176
|
+
if (found)
|
|
177
|
+
return { engine: "rg", rgBin: found };
|
|
178
|
+
if (forced === "rg") {
|
|
179
|
+
// Forced rg with none findable: let the spawn error surface loudly.
|
|
180
|
+
return { engine: "rg", rgBin: "rg" };
|
|
181
|
+
}
|
|
182
|
+
if (rgInstallSupported()) {
|
|
183
|
+
if (ripgrepAutoInstall()) {
|
|
184
|
+
try {
|
|
185
|
+
const installed = await installRg((line) => process.stderr.write(`harnery: ${line}\n`));
|
|
186
|
+
return { engine: "rg", rgBin: installed };
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
process.stderr.write(`harnery: ripgrep auto-install failed (${err.message}); using grep fallback\n`);
|
|
190
|
+
return { engine: "grep", rgBin: "rg" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const bin = resolveBinName();
|
|
194
|
+
hintOncePerDay(`${bin} ${hintKey}: ripgrep not found; using the slower GNU grep fallback. ` +
|
|
195
|
+
`Run \`${bin} doctor --fix\` to install it (pinned + checksum-verified), ` +
|
|
196
|
+
`or set { "tools": { "ripgrep": { "autoInstall": true } } } in .harnery/config.jsonc.`);
|
|
197
|
+
}
|
|
198
|
+
return { engine: "grep", rgBin: "rg" };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Emit `line` to stderr at most once per 24h (stamp file in the tools dir).
|
|
202
|
+
* Keeps the "rg missing" nudge from nagging on every single grep invocation.
|
|
203
|
+
*/
|
|
204
|
+
export function hintOncePerDay(line) {
|
|
205
|
+
try {
|
|
206
|
+
const stamp = join(toolsDir(), ".rg-hint-stamp");
|
|
207
|
+
if (existsSync(stamp) && Date.now() - statSync(stamp).mtimeMs < 24 * 60 * 60 * 1000) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
mkdirSync(toolsDir(), { recursive: true });
|
|
211
|
+
writeFileSync(stamp, String(Date.now()));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// stamping failed; still emit the hint
|
|
215
|
+
}
|
|
216
|
+
process.stderr.write(`${line}\n`);
|
|
217
|
+
}
|
package/package.json
CHANGED
package/src/commander.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { registerSectionCommand, registerTocCommand } from "./commands/toc.ts";
|
|
|
49
49
|
import { registerTokensCommand } from "./commands/tokens.ts";
|
|
50
50
|
import { registerTunnelCommand } from "./commands/tunnel.ts";
|
|
51
51
|
import { registerWebCommand } from "./commands/web.ts";
|
|
52
|
+
import { registerWorkflowCommand } from "./commands/workflow.ts";
|
|
52
53
|
|
|
53
54
|
export interface HarneryContextOpts {
|
|
54
55
|
/**
|
|
@@ -120,6 +121,15 @@ export interface HarneryProgramContext {
|
|
|
120
121
|
* carrying provider-specific checks. harn standalone ships none.
|
|
121
122
|
*/
|
|
122
123
|
envSections?: Record<string, EnvSection>;
|
|
124
|
+
/**
|
|
125
|
+
* Extra directory names for `grep` to exclude by default — host-generated
|
|
126
|
+
* mirrors, vendored trees, and similar directories that only ever produce
|
|
127
|
+
* duplicate or noisy matches. Names match at any depth (same semantics as
|
|
128
|
+
* grep's `--exclude-dir`). Applied alongside the built-in skip list and
|
|
129
|
+
* disabled together with it by `--no-default-excludes`. harn standalone
|
|
130
|
+
* ships none.
|
|
131
|
+
*/
|
|
132
|
+
grepExcludeDirs?: readonly string[];
|
|
123
133
|
/**
|
|
124
134
|
* Optional callback that returns extra HTTP headers to attach to outbound
|
|
125
135
|
* `fetch` calls based on the target URL. Useful for consumers that need
|
|
@@ -268,6 +278,7 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
|
|
|
268
278
|
registerDeinitCommand(program, emit, opts.binName);
|
|
269
279
|
registerBackupCommand(program, emit);
|
|
270
280
|
registerSyncCommand(program, emit);
|
|
281
|
+
registerWorkflowCommand(program, emit);
|
|
271
282
|
if (include("web")) registerWebCommand(program, emit);
|
|
272
283
|
|
|
273
284
|
return program;
|