cc-agent-harness 0.0.1
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/LICENSE +21 -0
- package/README.md +236 -0
- package/README.zh-CN.md +236 -0
- package/dist/chunk-3P72TGGQ.js +82 -0
- package/dist/chunk-3P72TGGQ.js.map +1 -0
- package/dist/chunk-JKJ3FP6T.js +51 -0
- package/dist/chunk-JKJ3FP6T.js.map +1 -0
- package/dist/chunk-LOE6IDTT.js +65 -0
- package/dist/chunk-LOE6IDTT.js.map +1 -0
- package/dist/chunk-PQWK2OBN.js +18 -0
- package/dist/chunk-PQWK2OBN.js.map +1 -0
- package/dist/chunk-R6VGYQOH.js +771 -0
- package/dist/chunk-R6VGYQOH.js.map +1 -0
- package/dist/config-PUMLWJWT.js +46 -0
- package/dist/config-PUMLWJWT.js.map +1 -0
- package/dist/context-SRWWNVTI.js +33 -0
- package/dist/context-SRWWNVTI.js.map +1 -0
- package/dist/doctor-TYLZH27K.js +199 -0
- package/dist/doctor-TYLZH27K.js.map +1 -0
- package/dist/harness.js +62 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +846 -0
- package/dist/index.js +1181 -0
- package/dist/index.js.map +1 -0
- package/dist/list-ABNY2TO7.js +124 -0
- package/dist/list-ABNY2TO7.js.map +1 -0
- package/dist/loader-RDQZFHOB.js +19 -0
- package/dist/loader-RDQZFHOB.js.map +1 -0
- package/dist/run-I5GVJH3N.js +45 -0
- package/dist/run-I5GVJH3N.js.map +1 -0
- package/dist/scaffold-C6JA3STC.js +98 -0
- package/dist/scaffold-C6JA3STC.js.map +1 -0
- package/dist/schema-AHX7LXUQ.js +27 -0
- package/dist/schema-AHX7LXUQ.js.map +1 -0
- package/dist/setup-CWDCKM34.js +115 -0
- package/dist/setup-CWDCKM34.js.map +1 -0
- package/dist/update-CTPDQCLU.js +87 -0
- package/dist/update-CTPDQCLU.js.map +1 -0
- package/dist/verify-NI3VCE2H.js +136 -0
- package/dist/verify-NI3VCE2H.js.map +1 -0
- package/package.json +61 -0
- package/templates/agents-md/full.md.tmpl +98 -0
- package/templates/agents-md/minimal.md.tmpl +20 -0
- package/templates/agents-md/standard.md.tmpl +43 -0
- package/templates/configs/harness.config.yaml.tmpl +28 -0
- package/templates/skills/basic/SKILL.md.tmpl +14 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
anyConfigExists,
|
|
4
|
+
loadConfig,
|
|
5
|
+
projectConfigExists
|
|
6
|
+
} from "./chunk-3P72TGGQ.js";
|
|
7
|
+
|
|
8
|
+
// src/adapter/detector.ts
|
|
9
|
+
import { existsSync as existsSync4 } from "fs";
|
|
10
|
+
import { join as join4 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/adapter/rust.ts
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var RustAdapter = class {
|
|
16
|
+
name = "rust";
|
|
17
|
+
async detect(cwd) {
|
|
18
|
+
return existsSync(join(cwd, "Cargo.toml"));
|
|
19
|
+
}
|
|
20
|
+
getCommands() {
|
|
21
|
+
return [
|
|
22
|
+
{ name: "fmt", command: "cargo fmt", description: "Format Rust code" },
|
|
23
|
+
{ name: "test", command: "cargo test", description: "Run Rust tests" },
|
|
24
|
+
{ name: "clippy", command: "cargo clippy --tests", description: "Run Clippy lints" },
|
|
25
|
+
{ name: "build", command: "cargo build", description: "Build project" }
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
getHealthChecks() {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: "cargo-installed",
|
|
32
|
+
check: async () => {
|
|
33
|
+
try {
|
|
34
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
35
|
+
execSync2("cargo --version", { stdio: "pipe" });
|
|
36
|
+
return { status: "pass", message: "cargo is available" };
|
|
37
|
+
} catch {
|
|
38
|
+
return { status: "fail", message: "cargo not found in PATH" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/adapter/typescript.ts
|
|
47
|
+
import { existsSync as existsSync2 } from "fs";
|
|
48
|
+
import { join as join2 } from "path";
|
|
49
|
+
var TypeScriptAdapter = class {
|
|
50
|
+
name = "typescript";
|
|
51
|
+
async detect(cwd) {
|
|
52
|
+
return existsSync2(join2(cwd, "tsconfig.json")) || existsSync2(join2(cwd, "package.json"));
|
|
53
|
+
}
|
|
54
|
+
getCommands() {
|
|
55
|
+
return [
|
|
56
|
+
{ name: "build", command: "npm run build", description: "Build TypeScript project" },
|
|
57
|
+
{ name: "test", command: "npm test", description: "Run tests" },
|
|
58
|
+
{ name: "lint", command: "npm run lint", description: "Run linter" }
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
getHealthChecks() {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
name: "node-version",
|
|
65
|
+
check: async () => {
|
|
66
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
67
|
+
if (major >= 22) {
|
|
68
|
+
return { status: "pass", message: `Node.js v${process.versions.node}` };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
status: "warn",
|
|
72
|
+
message: `Node.js v${process.versions.node} (>=22 recommended)`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/adapter/python.ts
|
|
81
|
+
import { existsSync as existsSync3 } from "fs";
|
|
82
|
+
import { join as join3 } from "path";
|
|
83
|
+
var PythonAdapter = class {
|
|
84
|
+
name = "python";
|
|
85
|
+
async detect(cwd) {
|
|
86
|
+
return existsSync3(join3(cwd, "pyproject.toml")) || existsSync3(join3(cwd, "setup.py")) || existsSync3(join3(cwd, "requirements.txt"));
|
|
87
|
+
}
|
|
88
|
+
getCommands() {
|
|
89
|
+
return [
|
|
90
|
+
{ name: "test", command: "pytest", description: "Run Python tests" },
|
|
91
|
+
{ name: "lint", command: "ruff check .", description: "Run linter" },
|
|
92
|
+
{ name: "fmt", command: "ruff format .", description: "Format Python code" }
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
getHealthChecks() {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
name: "python-installed",
|
|
99
|
+
check: async () => {
|
|
100
|
+
try {
|
|
101
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
102
|
+
const out = execSync2("python3 --version", { stdio: "pipe" }).toString().trim();
|
|
103
|
+
return { status: "pass", message: out };
|
|
104
|
+
} catch {
|
|
105
|
+
return { status: "fail", message: "python3 not found in PATH" };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/adapter/registry.ts
|
|
114
|
+
var BUILT_IN_ADAPTERS = [
|
|
115
|
+
new RustAdapter(),
|
|
116
|
+
new TypeScriptAdapter(),
|
|
117
|
+
new PythonAdapter()
|
|
118
|
+
];
|
|
119
|
+
var AdapterRegistry = class {
|
|
120
|
+
adapters = /* @__PURE__ */ new Map();
|
|
121
|
+
constructor(additionalAdapters = []) {
|
|
122
|
+
for (const adapter of BUILT_IN_ADAPTERS) {
|
|
123
|
+
this.adapters.set(adapter.name, adapter);
|
|
124
|
+
}
|
|
125
|
+
for (const adapter of additionalAdapters) {
|
|
126
|
+
this.adapters.set(adapter.name, adapter);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
register(adapter) {
|
|
130
|
+
this.adapters.set(adapter.name, adapter);
|
|
131
|
+
}
|
|
132
|
+
get(name) {
|
|
133
|
+
return this.adapters.get(name);
|
|
134
|
+
}
|
|
135
|
+
list() {
|
|
136
|
+
return [...this.adapters.values()];
|
|
137
|
+
}
|
|
138
|
+
async detect(cwd) {
|
|
139
|
+
for (const adapter of this.adapters.values()) {
|
|
140
|
+
if (await adapter.detect(cwd)) return adapter;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
count() {
|
|
145
|
+
const builtin = BUILT_IN_ADAPTERS.length;
|
|
146
|
+
const total = this.adapters.size;
|
|
147
|
+
return { builtin, custom: total - builtin, total };
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// src/adapter/detector.ts
|
|
152
|
+
var defaultRegistry = new AdapterRegistry();
|
|
153
|
+
async function detectProjectType(cwd) {
|
|
154
|
+
return defaultRegistry.detect(cwd);
|
|
155
|
+
}
|
|
156
|
+
function detectLanguage(cwd) {
|
|
157
|
+
const signals = {
|
|
158
|
+
rust: existsSync4(join4(cwd, "Cargo.toml")),
|
|
159
|
+
typescript: existsSync4(join4(cwd, "package.json")) || existsSync4(join4(cwd, "tsconfig.json")),
|
|
160
|
+
python: existsSync4(join4(cwd, "pyproject.toml")) || existsSync4(join4(cwd, "setup.py")) || existsSync4(join4(cwd, "requirements.txt"))
|
|
161
|
+
};
|
|
162
|
+
const detected = Object.entries(signals).filter(([, v]) => v);
|
|
163
|
+
if (detected.length > 1) return "multi";
|
|
164
|
+
if (detected.length === 1) return detected[0][0];
|
|
165
|
+
return "typescript";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/config/defaults.ts
|
|
169
|
+
var BUILT_IN_AGENTS = [
|
|
170
|
+
{ name: "architect-low", domain: "analysis", tier: "low" },
|
|
171
|
+
{ name: "architect-medium", domain: "analysis", tier: "medium" },
|
|
172
|
+
{ name: "architect", domain: "analysis", tier: "high" },
|
|
173
|
+
{ name: "executor-low", domain: "execution", tier: "low" },
|
|
174
|
+
{ name: "executor", domain: "execution", tier: "medium" },
|
|
175
|
+
{ name: "executor-high", domain: "execution", tier: "high" },
|
|
176
|
+
{ name: "explore", domain: "search", tier: "low" },
|
|
177
|
+
{ name: "explore-medium", domain: "search", tier: "medium" },
|
|
178
|
+
{ name: "explore-high", domain: "search", tier: "high" },
|
|
179
|
+
{ name: "researcher-low", domain: "research", tier: "low" },
|
|
180
|
+
{ name: "researcher", domain: "research", tier: "medium" },
|
|
181
|
+
{ name: "designer-low", domain: "frontend", tier: "low" },
|
|
182
|
+
{ name: "designer", domain: "frontend", tier: "medium" },
|
|
183
|
+
{ name: "designer-high", domain: "frontend", tier: "high" },
|
|
184
|
+
{ name: "writer", domain: "docs", tier: "low" },
|
|
185
|
+
{ name: "vision", domain: "visual", tier: "medium" },
|
|
186
|
+
{ name: "planner", domain: "planning", tier: "high" },
|
|
187
|
+
{ name: "critic", domain: "critique", tier: "high" },
|
|
188
|
+
{ name: "analyst", domain: "pre-planning", tier: "high" },
|
|
189
|
+
{ name: "qa-tester", domain: "testing", tier: "medium" },
|
|
190
|
+
{ name: "qa-tester-high", domain: "testing", tier: "high" },
|
|
191
|
+
{ name: "security-reviewer-low", domain: "security", tier: "low" },
|
|
192
|
+
{ name: "security-reviewer", domain: "security", tier: "high" },
|
|
193
|
+
{ name: "build-fixer-low", domain: "build", tier: "low" },
|
|
194
|
+
{ name: "build-fixer", domain: "build", tier: "medium" },
|
|
195
|
+
{ name: "tdd-guide-low", domain: "tdd", tier: "low" },
|
|
196
|
+
{ name: "tdd-guide", domain: "tdd", tier: "medium" },
|
|
197
|
+
{ name: "code-reviewer-low", domain: "code-review", tier: "low" },
|
|
198
|
+
{ name: "code-reviewer", domain: "code-review", tier: "high" },
|
|
199
|
+
{ name: "scientist-low", domain: "data-science", tier: "low" },
|
|
200
|
+
{ name: "scientist", domain: "data-science", tier: "medium" },
|
|
201
|
+
{ name: "scientist-high", domain: "data-science", tier: "high" }
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// src/agent/registry.ts
|
|
205
|
+
var AgentRegistry = class {
|
|
206
|
+
agents;
|
|
207
|
+
constructor(customAgents = []) {
|
|
208
|
+
this.agents = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const agent of BUILT_IN_AGENTS) {
|
|
210
|
+
this.agents.set(agent.name, agent);
|
|
211
|
+
}
|
|
212
|
+
for (const agent of customAgents) {
|
|
213
|
+
this.agents.set(agent.name, agent);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
get(name) {
|
|
217
|
+
return this.agents.get(name);
|
|
218
|
+
}
|
|
219
|
+
list() {
|
|
220
|
+
return [...this.agents.values()];
|
|
221
|
+
}
|
|
222
|
+
listByDomain(domain) {
|
|
223
|
+
return this.list().filter((a) => a.domain === domain);
|
|
224
|
+
}
|
|
225
|
+
listByTier(tier) {
|
|
226
|
+
return this.list().filter((a) => a.tier === tier);
|
|
227
|
+
}
|
|
228
|
+
domains() {
|
|
229
|
+
return [...new Set(this.list().map((a) => a.domain))];
|
|
230
|
+
}
|
|
231
|
+
count() {
|
|
232
|
+
const builtin = BUILT_IN_AGENTS.length;
|
|
233
|
+
const total = this.agents.size;
|
|
234
|
+
return { builtin, custom: total - builtin, total };
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/skill/manager.ts
|
|
239
|
+
import { readdir, stat } from "fs/promises";
|
|
240
|
+
import { join as join6, resolve } from "path";
|
|
241
|
+
import { existsSync as existsSync6 } from "fs";
|
|
242
|
+
|
|
243
|
+
// src/skill/validator.ts
|
|
244
|
+
import { readFile } from "fs/promises";
|
|
245
|
+
import { join as join5 } from "path";
|
|
246
|
+
import { existsSync as existsSync5 } from "fs";
|
|
247
|
+
async function validateSkill(skillDir) {
|
|
248
|
+
const errors = [];
|
|
249
|
+
const warnings = [];
|
|
250
|
+
let name = null;
|
|
251
|
+
let description = null;
|
|
252
|
+
const skillMdPath = join5(skillDir, "SKILL.md");
|
|
253
|
+
if (!existsSync5(skillMdPath)) {
|
|
254
|
+
errors.push("Missing SKILL.md");
|
|
255
|
+
return { valid: false, name, description, errors, warnings };
|
|
256
|
+
}
|
|
257
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
258
|
+
const frontmatter = extractFrontmatter(content);
|
|
259
|
+
if (!frontmatter) {
|
|
260
|
+
errors.push("SKILL.md missing YAML frontmatter (---...--- block)");
|
|
261
|
+
return { valid: false, name, description, errors, warnings };
|
|
262
|
+
}
|
|
263
|
+
name = frontmatter.name ?? null;
|
|
264
|
+
description = frontmatter.description ?? null;
|
|
265
|
+
if (!name) errors.push("Frontmatter missing required 'name' field");
|
|
266
|
+
if (!description) errors.push("Frontmatter missing required 'description' field");
|
|
267
|
+
const metadataYaml = join5(skillDir, "metadata.yaml");
|
|
268
|
+
if (!existsSync5(metadataYaml)) {
|
|
269
|
+
warnings.push("No metadata.yaml (recommended for skill metadata)");
|
|
270
|
+
}
|
|
271
|
+
return { valid: errors.length === 0, name, description, errors, warnings };
|
|
272
|
+
}
|
|
273
|
+
function extractFrontmatter(content) {
|
|
274
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
275
|
+
if (!match) return null;
|
|
276
|
+
const fields = {};
|
|
277
|
+
const lines = match[1].split("\n");
|
|
278
|
+
let currentKey = null;
|
|
279
|
+
let currentValue = "";
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
|
|
282
|
+
if (kvMatch) {
|
|
283
|
+
if (currentKey) fields[currentKey] = currentValue.trim();
|
|
284
|
+
currentKey = kvMatch[1];
|
|
285
|
+
currentValue = kvMatch[2];
|
|
286
|
+
} else if (currentKey && (line.startsWith(" ") || line.startsWith(" "))) {
|
|
287
|
+
currentValue += " " + line.trim();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (currentKey) fields[currentKey] = currentValue.trim();
|
|
291
|
+
return fields;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/skill/manager.ts
|
|
295
|
+
async function discoverSkills(directories, cwd) {
|
|
296
|
+
const base = cwd ?? process.cwd();
|
|
297
|
+
const skills = [];
|
|
298
|
+
for (const dir of directories) {
|
|
299
|
+
const absDir = resolve(base, dir);
|
|
300
|
+
if (!existsSync6(absDir)) continue;
|
|
301
|
+
const entries = await readdir(absDir);
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
const skillDir = join6(absDir, entry);
|
|
304
|
+
const info = await stat(skillDir);
|
|
305
|
+
if (!info.isDirectory()) continue;
|
|
306
|
+
const skillMd = join6(skillDir, "SKILL.md");
|
|
307
|
+
if (!existsSync6(skillMd)) continue;
|
|
308
|
+
const validation = await validateSkill(skillDir);
|
|
309
|
+
skills.push({
|
|
310
|
+
name: validation.name ?? entry,
|
|
311
|
+
description: validation.description ?? "",
|
|
312
|
+
path: skillDir,
|
|
313
|
+
valid: validation.valid
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return skills;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/runtime/harness.ts
|
|
321
|
+
import { dirname as dirname3, resolve as resolve5 } from "path";
|
|
322
|
+
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
323
|
+
|
|
324
|
+
// src/audit/logger.ts
|
|
325
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
326
|
+
import { resolve as resolve2, dirname } from "path";
|
|
327
|
+
var DEFAULT_LOG_DIR = ".harness/logs";
|
|
328
|
+
var LOG_FILE = "audit.jsonl";
|
|
329
|
+
var AuditLogger = class {
|
|
330
|
+
logPath;
|
|
331
|
+
constructor(cwd, logDir) {
|
|
332
|
+
this.logPath = resolve2(cwd, logDir ?? DEFAULT_LOG_DIR, LOG_FILE);
|
|
333
|
+
}
|
|
334
|
+
async log(kind, message, data) {
|
|
335
|
+
const entry = {
|
|
336
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
337
|
+
kind,
|
|
338
|
+
message,
|
|
339
|
+
data
|
|
340
|
+
};
|
|
341
|
+
await mkdir(dirname(this.logPath), { recursive: true });
|
|
342
|
+
await appendFile(this.logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
343
|
+
}
|
|
344
|
+
getLogPath() {
|
|
345
|
+
return this.logPath;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/context/pipeline.ts
|
|
350
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
351
|
+
import { existsSync as existsSync7 } from "fs";
|
|
352
|
+
import { resolve as resolve3, dirname as dirname2, relative, sep } from "path";
|
|
353
|
+
var ContextPipeline = class {
|
|
354
|
+
blocks = [];
|
|
355
|
+
addBlock(tag, content, priority = 50) {
|
|
356
|
+
this.blocks.push({ tag, content, priority });
|
|
357
|
+
}
|
|
358
|
+
async addProjectDoc(cwd) {
|
|
359
|
+
const agentsMd = resolve3(cwd, "AGENTS.md");
|
|
360
|
+
if (existsSync7(agentsMd)) {
|
|
361
|
+
const content = await readFile2(agentsMd, "utf-8");
|
|
362
|
+
this.addBlock("project-doc", content, 10);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Walk from projectRoot to cwd, collecting AGENTS.md files.
|
|
367
|
+
* Deeper files override shallower ones (later blocks have higher priority).
|
|
368
|
+
*/
|
|
369
|
+
async addHierarchicalDocs(cwd, projectRoot) {
|
|
370
|
+
const root = projectRoot ?? await findProjectRoot(cwd);
|
|
371
|
+
const docs = collectAgentsMdFiles(root, cwd);
|
|
372
|
+
for (let i = 0; i < docs.length; i++) {
|
|
373
|
+
const filePath = docs[i];
|
|
374
|
+
const content = await readFile2(filePath, "utf-8");
|
|
375
|
+
const depth = i;
|
|
376
|
+
this.addBlock(`agents-md-${depth}`, content, 10 + depth);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async addSkillsSummary(config, cwd) {
|
|
380
|
+
const skills = await discoverSkills(config.skills.directories, cwd);
|
|
381
|
+
if (skills.length === 0) return;
|
|
382
|
+
const lines = ["Available skills:"];
|
|
383
|
+
for (const skill of skills) {
|
|
384
|
+
if (skill.valid) {
|
|
385
|
+
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.addBlock("skills", lines.join("\n"), 30);
|
|
389
|
+
}
|
|
390
|
+
addCustomRules(rules) {
|
|
391
|
+
if (rules.length === 0) return;
|
|
392
|
+
const content = rules.map((r) => `- ${r}`).join("\n");
|
|
393
|
+
this.addBlock("custom-rules", content, 20);
|
|
394
|
+
}
|
|
395
|
+
addRaw(tag, content, priority) {
|
|
396
|
+
this.addBlock(tag, content, priority);
|
|
397
|
+
}
|
|
398
|
+
build(options) {
|
|
399
|
+
const sorted = [...this.blocks].sort((a, b) => a.priority - b.priority);
|
|
400
|
+
const tagStyle = options?.tagStyle ?? "markdown";
|
|
401
|
+
const parts = [];
|
|
402
|
+
for (const block of sorted) {
|
|
403
|
+
const [open, close] = formatTags(block.tag, tagStyle);
|
|
404
|
+
if (open) parts.push(open);
|
|
405
|
+
parts.push(block.content);
|
|
406
|
+
if (close) parts.push(close);
|
|
407
|
+
parts.push("");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
blocks: sorted,
|
|
411
|
+
rendered: parts.join("\n").trim()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
clear() {
|
|
415
|
+
this.blocks = [];
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
function formatTags(tag, style) {
|
|
419
|
+
switch (style) {
|
|
420
|
+
case "xml":
|
|
421
|
+
return [`<${tag}>`, `</${tag}>`];
|
|
422
|
+
case "markdown":
|
|
423
|
+
return [`<!-- ${tag} -->`, `<!-- /${tag} -->`];
|
|
424
|
+
case "none":
|
|
425
|
+
return [null, null];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function collectAgentsMdFiles(root, cwd) {
|
|
429
|
+
const files = [];
|
|
430
|
+
let current = resolve3(root);
|
|
431
|
+
const target = resolve3(cwd);
|
|
432
|
+
const steps = relative(current, target).split(sep).filter(Boolean);
|
|
433
|
+
const collect = (dir) => {
|
|
434
|
+
const agentsMd = resolve3(dir, "AGENTS.md");
|
|
435
|
+
if (existsSync7(agentsMd)) {
|
|
436
|
+
files.push(agentsMd);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
collect(current);
|
|
440
|
+
for (const step of steps) {
|
|
441
|
+
current = resolve3(current, step);
|
|
442
|
+
if (!target.startsWith(current)) break;
|
|
443
|
+
collect(current);
|
|
444
|
+
}
|
|
445
|
+
return files;
|
|
446
|
+
}
|
|
447
|
+
async function findProjectRoot(cwd) {
|
|
448
|
+
let dir = resolve3(cwd);
|
|
449
|
+
while (true) {
|
|
450
|
+
if (existsSync7(resolve3(dir, ".harness")) || existsSync7(resolve3(dir, ".git"))) {
|
|
451
|
+
return dir;
|
|
452
|
+
}
|
|
453
|
+
const parent = dirname2(dir);
|
|
454
|
+
if (parent === dir) return cwd;
|
|
455
|
+
dir = parent;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/feature/registry.ts
|
|
460
|
+
var FeatureRegistry = class {
|
|
461
|
+
specs = /* @__PURE__ */ new Map();
|
|
462
|
+
overrides = /* @__PURE__ */ new Map();
|
|
463
|
+
aliases = /* @__PURE__ */ new Map();
|
|
464
|
+
aliasUsages = [];
|
|
465
|
+
register(spec) {
|
|
466
|
+
this.specs.set(spec.id, spec);
|
|
467
|
+
}
|
|
468
|
+
registerMany(specs) {
|
|
469
|
+
for (const spec of specs) {
|
|
470
|
+
this.register(spec);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
registerAlias(legacyKey, featureId) {
|
|
474
|
+
this.aliases.set(legacyKey, featureId);
|
|
475
|
+
}
|
|
476
|
+
registerAliases(aliasMap) {
|
|
477
|
+
for (const [legacy, id] of Object.entries(aliasMap)) {
|
|
478
|
+
this.registerAlias(legacy, id);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
applyOverrides(overrides) {
|
|
482
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
483
|
+
const spec = this.findByKey(key);
|
|
484
|
+
if (spec) {
|
|
485
|
+
if (spec.stage === "removed") continue;
|
|
486
|
+
this.overrides.set(spec.id, value);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
isEnabled(id) {
|
|
491
|
+
const resolved = this.resolveAlias(id);
|
|
492
|
+
const override = this.overrides.get(resolved);
|
|
493
|
+
if (override !== void 0) return override;
|
|
494
|
+
const spec = this.specs.get(resolved);
|
|
495
|
+
if (!spec) return false;
|
|
496
|
+
if (spec.stage === "removed") return false;
|
|
497
|
+
return spec.defaultEnabled;
|
|
498
|
+
}
|
|
499
|
+
get(id) {
|
|
500
|
+
const resolved = this.resolveAlias(id);
|
|
501
|
+
return this.specs.get(resolved);
|
|
502
|
+
}
|
|
503
|
+
list() {
|
|
504
|
+
return [...this.specs.values()];
|
|
505
|
+
}
|
|
506
|
+
listByStage(stage) {
|
|
507
|
+
return this.list().filter((s) => s.stage === stage);
|
|
508
|
+
}
|
|
509
|
+
listEnabled() {
|
|
510
|
+
return this.list().filter((s) => this.isEnabled(s.id));
|
|
511
|
+
}
|
|
512
|
+
count() {
|
|
513
|
+
const total = this.specs.size;
|
|
514
|
+
const enabled = this.listEnabled().length;
|
|
515
|
+
return { total, enabled, disabled: total - enabled };
|
|
516
|
+
}
|
|
517
|
+
getAliasUsages() {
|
|
518
|
+
return [...this.aliasUsages];
|
|
519
|
+
}
|
|
520
|
+
resolveAlias(key) {
|
|
521
|
+
const mapped = this.aliases.get(key);
|
|
522
|
+
if (mapped) {
|
|
523
|
+
this.aliasUsages.push(`Legacy key "${key}" resolved to "${mapped}"`);
|
|
524
|
+
return mapped;
|
|
525
|
+
}
|
|
526
|
+
return key;
|
|
527
|
+
}
|
|
528
|
+
findByKey(key) {
|
|
529
|
+
const resolved = this.resolveAlias(key);
|
|
530
|
+
const direct = this.specs.get(resolved);
|
|
531
|
+
if (direct) return direct;
|
|
532
|
+
for (const spec of this.specs.values()) {
|
|
533
|
+
if (spec.key === resolved || spec.id === resolved) return spec;
|
|
534
|
+
}
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/hook/discovery.ts
|
|
540
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
541
|
+
import { existsSync as existsSync8 } from "fs";
|
|
542
|
+
import { resolve as resolve4 } from "path";
|
|
543
|
+
import { parse as parseYaml } from "yaml";
|
|
544
|
+
var HOOKS_FILE_NAMES = ["hooks.yaml", "hooks.json"];
|
|
545
|
+
async function discoverHooks(cwd) {
|
|
546
|
+
const handlers = [];
|
|
547
|
+
const harnessDir = resolve4(cwd, ".harness");
|
|
548
|
+
for (const fileName of HOOKS_FILE_NAMES) {
|
|
549
|
+
const filePath = resolve4(harnessDir, fileName);
|
|
550
|
+
if (!existsSync8(filePath)) continue;
|
|
551
|
+
const content = await readFile3(filePath, "utf-8");
|
|
552
|
+
const parsed = fileName.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
|
|
553
|
+
if (parsed && Array.isArray(parsed.hooks)) {
|
|
554
|
+
for (const hook of parsed.hooks) {
|
|
555
|
+
if (hook.event && hook.command) {
|
|
556
|
+
handlers.push({
|
|
557
|
+
event: hook.event,
|
|
558
|
+
command: hook.command,
|
|
559
|
+
matcher: hook.matcher
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
return handlers;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/hook/dispatcher.ts
|
|
570
|
+
import { execSync } from "child_process";
|
|
571
|
+
var HookDispatcher = class {
|
|
572
|
+
handlers;
|
|
573
|
+
constructor(handlers) {
|
|
574
|
+
this.handlers = handlers;
|
|
575
|
+
}
|
|
576
|
+
async dispatch(event, cwd, data) {
|
|
577
|
+
const matching = this.handlers.filter((h) => h.event === event);
|
|
578
|
+
if (matching.length === 0) return [];
|
|
579
|
+
const payload = {
|
|
580
|
+
event,
|
|
581
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
582
|
+
cwd,
|
|
583
|
+
data
|
|
584
|
+
};
|
|
585
|
+
const payloadJson = JSON.stringify(payload);
|
|
586
|
+
const results = [];
|
|
587
|
+
for (const handler of matching) {
|
|
588
|
+
if (handler.matcher && data) {
|
|
589
|
+
const matchValue = String(data.match ?? "");
|
|
590
|
+
if (matchValue && !matchValue.includes(handler.matcher)) continue;
|
|
591
|
+
}
|
|
592
|
+
const start = Date.now();
|
|
593
|
+
try {
|
|
594
|
+
const stdout = execSync(handler.command, {
|
|
595
|
+
cwd,
|
|
596
|
+
input: payloadJson,
|
|
597
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
598
|
+
timeout: 3e4
|
|
599
|
+
}).toString();
|
|
600
|
+
results.push({
|
|
601
|
+
handler,
|
|
602
|
+
exitCode: 0,
|
|
603
|
+
stdout,
|
|
604
|
+
stderr: "",
|
|
605
|
+
durationMs: Date.now() - start
|
|
606
|
+
});
|
|
607
|
+
} catch (err) {
|
|
608
|
+
const execErr = err;
|
|
609
|
+
results.push({
|
|
610
|
+
handler,
|
|
611
|
+
exitCode: execErr.status ?? 1,
|
|
612
|
+
stdout: execErr.stdout?.toString() ?? "",
|
|
613
|
+
stderr: execErr.stderr?.toString() ?? "",
|
|
614
|
+
durationMs: Date.now() - start
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return results;
|
|
619
|
+
}
|
|
620
|
+
hasHandlers(event) {
|
|
621
|
+
return this.handlers.some((h) => h.event === event);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// src/runtime/harness.ts
|
|
626
|
+
var HarnessRuntime = class _HarnessRuntime {
|
|
627
|
+
cwd;
|
|
628
|
+
hasProjectConfig;
|
|
629
|
+
hasAnyConfig;
|
|
630
|
+
config;
|
|
631
|
+
adapter;
|
|
632
|
+
agentRegistry;
|
|
633
|
+
featureRegistry;
|
|
634
|
+
hookDispatcher;
|
|
635
|
+
auditLogger;
|
|
636
|
+
constructor(args) {
|
|
637
|
+
this.cwd = args.cwd;
|
|
638
|
+
this.hasProjectConfig = args.hasProjectConfig;
|
|
639
|
+
this.hasAnyConfig = args.hasAnyConfig;
|
|
640
|
+
this.config = args.config;
|
|
641
|
+
this.adapter = args.adapter;
|
|
642
|
+
this.hookDispatcher = args.hookDispatcher;
|
|
643
|
+
this.auditLogger = args.auditLogger;
|
|
644
|
+
this.featureRegistry = args.featureRegistry;
|
|
645
|
+
this.agentRegistry = new AgentRegistry(args.config?.agents.definitions ?? []);
|
|
646
|
+
}
|
|
647
|
+
static async create(opts) {
|
|
648
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
649
|
+
const hasProjectConfig = projectConfigExists(cwd);
|
|
650
|
+
const hasAnyConfig = anyConfigExists(cwd);
|
|
651
|
+
if (opts?.requireProjectConfig && !hasProjectConfig) {
|
|
652
|
+
throw new Error("No harness project configuration found. Run `agent-harness setup` first.");
|
|
653
|
+
}
|
|
654
|
+
const config = hasProjectConfig ? await loadConfig({ cwd }) : void 0;
|
|
655
|
+
const adapter = await detectProjectType(cwd);
|
|
656
|
+
const handlers = hasProjectConfig ? await discoverHooks(cwd) : [];
|
|
657
|
+
const featureRegistry = buildFeatureRegistry(config);
|
|
658
|
+
return new _HarnessRuntime({
|
|
659
|
+
cwd,
|
|
660
|
+
hasProjectConfig,
|
|
661
|
+
hasAnyConfig,
|
|
662
|
+
config,
|
|
663
|
+
adapter,
|
|
664
|
+
hookDispatcher: new HookDispatcher(handlers),
|
|
665
|
+
auditLogger: new AuditLogger(cwd),
|
|
666
|
+
featureRegistry
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
listTasks() {
|
|
670
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
671
|
+
if (this.adapter) {
|
|
672
|
+
for (const command of this.adapter.getCommands()) {
|
|
673
|
+
tasks.set(command.name, toRuntimeTask(command, "adapter", this.adapter.name));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (this.config) {
|
|
677
|
+
for (const [name, command] of Object.entries(this.config.workflows.commands)) {
|
|
678
|
+
tasks.set(name, {
|
|
679
|
+
name,
|
|
680
|
+
command,
|
|
681
|
+
description: `Workflow command "${name}"`,
|
|
682
|
+
source: "workflow"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return [...tasks.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
687
|
+
}
|
|
688
|
+
getTask(name) {
|
|
689
|
+
return this.listTasks().find((task) => task.name === name);
|
|
690
|
+
}
|
|
691
|
+
listFeatureStates() {
|
|
692
|
+
const configuredFeatures = this.config?.features ?? {};
|
|
693
|
+
return this.featureRegistry.list().map((spec) => ({
|
|
694
|
+
spec,
|
|
695
|
+
enabled: this.featureRegistry.isEnabled(spec.id),
|
|
696
|
+
configured: Object.prototype.hasOwnProperty.call(configuredFeatures, spec.key),
|
|
697
|
+
configuredValue: configuredFeatures[spec.key]
|
|
698
|
+
}));
|
|
699
|
+
}
|
|
700
|
+
async dispatchHooks(event, data) {
|
|
701
|
+
if (!this.hasProjectConfig || !this.hookDispatcher.hasHandlers(event)) {
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
const results = await this.hookDispatcher.dispatch(event, this.cwd, data);
|
|
705
|
+
await this.log("hook.executed", `Executed ${results.length} hook(s) for ${event}`, {
|
|
706
|
+
event,
|
|
707
|
+
results: results.map((result) => ({
|
|
708
|
+
command: result.handler.command,
|
|
709
|
+
exitCode: result.exitCode,
|
|
710
|
+
durationMs: result.durationMs
|
|
711
|
+
}))
|
|
712
|
+
});
|
|
713
|
+
return results;
|
|
714
|
+
}
|
|
715
|
+
async log(kind, message, data) {
|
|
716
|
+
if (!this.hasProjectConfig && kind !== "setup") {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
await this.auditLogger.log(kind, message, data);
|
|
720
|
+
}
|
|
721
|
+
async buildContext(opts) {
|
|
722
|
+
const pipeline = new ContextPipeline();
|
|
723
|
+
await pipeline.addHierarchicalDocs(this.cwd);
|
|
724
|
+
if (this.config && (opts?.includeCustomRules ?? true)) {
|
|
725
|
+
pipeline.addCustomRules(this.config.templates.agents_md.custom_rules);
|
|
726
|
+
}
|
|
727
|
+
if (this.config && (opts?.includeSkills ?? true)) {
|
|
728
|
+
await pipeline.addSkillsSummary(this.config, this.cwd);
|
|
729
|
+
}
|
|
730
|
+
return pipeline.build({ tagStyle: opts?.tagStyle ?? "markdown" });
|
|
731
|
+
}
|
|
732
|
+
async writeContext(opts) {
|
|
733
|
+
const result = await this.buildContext(opts);
|
|
734
|
+
const outputPath = resolve5(this.cwd, opts.outputPath);
|
|
735
|
+
await mkdir2(dirname3(outputPath), { recursive: true });
|
|
736
|
+
await writeFile(outputPath, result.rendered + "\n", "utf-8");
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function toRuntimeTask(command, source, adapterName) {
|
|
741
|
+
return {
|
|
742
|
+
name: command.name,
|
|
743
|
+
command: command.command,
|
|
744
|
+
description: command.description,
|
|
745
|
+
source,
|
|
746
|
+
adapterName
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function buildFeatureRegistry(config) {
|
|
750
|
+
const registry = new FeatureRegistry();
|
|
751
|
+
const configured = config?.features ?? {};
|
|
752
|
+
for (const key of Object.keys(configured)) {
|
|
753
|
+
registry.register({
|
|
754
|
+
id: key,
|
|
755
|
+
key,
|
|
756
|
+
stage: "experimental",
|
|
757
|
+
defaultEnabled: false,
|
|
758
|
+
description: `Feature flag configured via "${key}"`
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
registry.applyOverrides(configured);
|
|
762
|
+
return registry;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export {
|
|
766
|
+
detectLanguage,
|
|
767
|
+
AgentRegistry,
|
|
768
|
+
discoverSkills,
|
|
769
|
+
HarnessRuntime
|
|
770
|
+
};
|
|
771
|
+
//# sourceMappingURL=chunk-R6VGYQOH.js.map
|