ai-dev-harness 0.1.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 +131 -0
- package/dist/agent.js +100 -0
- package/dist/audit.js +21 -0
- package/dist/cli.js +279 -0
- package/dist/config.js +156 -0
- package/dist/context.js +114 -0
- package/dist/init.js +29 -0
- package/dist/integration.js +173 -0
- package/dist/mcp.js +145 -0
- package/dist/runPaths.js +21 -0
- package/dist/summarize.js +72 -0
- package/dist/templates.js +217 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +95 -0
- package/dist/verify.js +28 -0
- package/package.json +40 -0
- package/scripts/read-evidence.ps1 +36 -0
- package/scripts/run-harness.ps1 +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# AI Dev Harness
|
|
2
|
+
|
|
3
|
+
Auditable TypeScript CLI harness that wraps Codex CLI or Claude Code, retrieves context from CodeGraph and Knowledge Graph MCP services, verifies changes, and writes local run evidence.
|
|
4
|
+
|
|
5
|
+
This repository is a distributable Harness package:
|
|
6
|
+
|
|
7
|
+
- CLI: `dist/cli.js`
|
|
8
|
+
- Claude/Codex conversation integration installer
|
|
9
|
+
- Claude Skill package installed into `.claude/skills/ai-dev-harness/`
|
|
10
|
+
- Agent helper scripts installed into `.harness/ai-dev-harness/scripts/`
|
|
11
|
+
- Git-native project skills installed into `skills/`
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
node dist/cli.js init
|
|
19
|
+
node dist/cli.js run --task "fix a small bug" --skill fix_bug --agent codex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI is intended to be installed inside a business Git repository. `init` creates `harness.yaml`, `skills/`, `AGENTS.md`, `TOOLS.md`, `SKILLS.md`, and `runs/`.
|
|
23
|
+
`harness run` must execute inside a Git repository; if it is launched elsewhere, the harness writes intake/audit evidence and exits nonzero.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node dist/cli.js init
|
|
29
|
+
node dist/cli.js run --task "add refund flow tests" --skill write_tests --agent claude
|
|
30
|
+
node dist/cli.js run --task "analyze download startup flow. Do not modify repository files." --skill update_docs --agent claude --live
|
|
31
|
+
node dist/cli.js verify --run <run_id_or_path>
|
|
32
|
+
node dist/cli.js summarize --run <run_id_or_path>
|
|
33
|
+
node dist/cli.js install-integration --agent both --target <repo>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Claude/Codex Conversation Trigger
|
|
37
|
+
|
|
38
|
+
Install conversation integration into a business repository:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node "E:\ai native\dist\cli.js" install-integration --agent both --target "C:\path\to\repo"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This creates:
|
|
45
|
+
|
|
46
|
+
- `.claude/skills/ai-dev-harness/SKILL.md` for Claude Code.
|
|
47
|
+
- An `AGENTS.md` Harness section for Codex.
|
|
48
|
+
- `.harness/ai-dev-harness/scripts/run-harness.ps1`
|
|
49
|
+
- `.harness/ai-dev-harness/scripts/read-evidence.ps1`
|
|
50
|
+
|
|
51
|
+
Then, inside Claude Code or Codex, ask naturally:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
Use AI Dev Harness to run update_docs: analyze the download startup flow. Do not modify repository files.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
or:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
Use Harness to run fix_bug: fix the download resume progress issue.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The generated integration uses `--live`, so the current conversation terminal can see Harness stages and agent output as the run progresses.
|
|
64
|
+
|
|
65
|
+
The installed Skill prefers the helper script:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
powershell -ExecutionPolicy Bypass -File ".harness/ai-dev-harness/scripts/run-harness.ps1" -Task "<task>" -Skill fix_bug -Agent claude -Live
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The direct CLI command remains available as a fallback:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node "E:\ai native\dist\cli.js" run --task "<task>" --skill fix_bug --agent claude --live
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Edit `harness.yaml` in the business repository:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
agents:
|
|
83
|
+
codex:
|
|
84
|
+
command: codex exec --full-auto -
|
|
85
|
+
claude:
|
|
86
|
+
command: claude -p -
|
|
87
|
+
mcp:
|
|
88
|
+
codegraph:
|
|
89
|
+
url: http://localhost:7331/mcp
|
|
90
|
+
knowledgeGraph:
|
|
91
|
+
url: http://localhost:7332/mcp
|
|
92
|
+
timeoutMs: 15000
|
|
93
|
+
verify:
|
|
94
|
+
defaultCommands:
|
|
95
|
+
- name: git status
|
|
96
|
+
command: git status --short
|
|
97
|
+
audit:
|
|
98
|
+
path: runs
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The agent command receives the generated prompt on stdin. CodeGraph and Knowledge Graph MCP are queried first; if either service is unavailable, the run is marked degraded and the harness falls back to local Git/file context.
|
|
102
|
+
If the agent command or verification fails, the run still writes all evidence files and then exits nonzero so CI can treat the run as failed.
|
|
103
|
+
|
|
104
|
+
## Run Evidence
|
|
105
|
+
|
|
106
|
+
Each `run` creates `runs/<run_id>/` with:
|
|
107
|
+
|
|
108
|
+
- `intake.json`
|
|
109
|
+
- `mcp_queries.jsonl`
|
|
110
|
+
- `context_pack.md`
|
|
111
|
+
- `agent_prompt.md`
|
|
112
|
+
- `plan.md`
|
|
113
|
+
- `execution.log`
|
|
114
|
+
- `git_diff.patch`
|
|
115
|
+
- `verify.json`
|
|
116
|
+
- `summary.md`
|
|
117
|
+
- `persist_suggestions.md`
|
|
118
|
+
- `audit.jsonl`
|
|
119
|
+
|
|
120
|
+
Knowledge Graph updates are not applied automatically in v1. Review `persist_suggestions.md` before writing them back.
|
|
121
|
+
|
|
122
|
+
## Engineering Prompt Contracts
|
|
123
|
+
|
|
124
|
+
Generated skills and agent prompts use a company-grade execution contract:
|
|
125
|
+
|
|
126
|
+
- Analysis Contract: restate the task, list assumptions, affected areas, risks, and missing context.
|
|
127
|
+
- Execution Contract: make only task-relevant minimal changes; no opportunistic refactors or broad cleanup.
|
|
128
|
+
- Verification Contract: run configured verification or record why checks could not run.
|
|
129
|
+
- Report Contract: summarize intent or root cause, changed files, verification, residual risk, and knowledge suggestions.
|
|
130
|
+
|
|
131
|
+
The test suite verifies that initialized skills and run evidence include these contracts.
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { git, runCommand, truncate } from "./utils.js";
|
|
3
|
+
function renderPrompt(input, contextPack) {
|
|
4
|
+
return `You are running inside an auditable AI development harness. Treat this as a company engineering task, not a casual chat.
|
|
5
|
+
|
|
6
|
+
Task:
|
|
7
|
+
${input.task}
|
|
8
|
+
|
|
9
|
+
Skill:
|
|
10
|
+
${input.skill.name}
|
|
11
|
+
|
|
12
|
+
Execution Contract:
|
|
13
|
+
1. Analysis first.
|
|
14
|
+
- Restate the task in one or two sentences.
|
|
15
|
+
- Identify assumptions, affected files/modules, relevant CodeGraph context, relevant Knowledge Graph context, and risks.
|
|
16
|
+
- If required context is missing or confidence is low, stop and report what is missing instead of guessing.
|
|
17
|
+
2. Plan before editing.
|
|
18
|
+
- Provide a minimal step-by-step plan.
|
|
19
|
+
- Keep the plan limited to the requested task.
|
|
20
|
+
3. Safe execution.
|
|
21
|
+
- Make the smallest task-relevant change.
|
|
22
|
+
- Do not perform opportunistic refactors, formatting-only sweeps, dependency upgrades, broad renames, or unrelated cleanup.
|
|
23
|
+
- Preserve public interfaces and backward compatibility unless explicitly instructed otherwise.
|
|
24
|
+
- Do not update Knowledge Graph directly.
|
|
25
|
+
4. Verification discipline.
|
|
26
|
+
- Harness will run configured verification after you finish.
|
|
27
|
+
- Only run extra commands when needed to understand or validate the task.
|
|
28
|
+
- Report any verification you could not run and why.
|
|
29
|
+
5. Required final report.
|
|
30
|
+
- Root cause or implementation intent.
|
|
31
|
+
- Files changed.
|
|
32
|
+
- Verification performed or deferred.
|
|
33
|
+
- Residual risk.
|
|
34
|
+
- Knowledge persistence suggestions.
|
|
35
|
+
|
|
36
|
+
Output Format:
|
|
37
|
+
## Impact Analysis
|
|
38
|
+
## Plan
|
|
39
|
+
## Execution Notes
|
|
40
|
+
## Verification Notes
|
|
41
|
+
## Final Report
|
|
42
|
+
|
|
43
|
+
Project Rules:
|
|
44
|
+
- Respect AGENTS.md, TOOLS.md, and SKILLS.md when present.
|
|
45
|
+
- Work inside this repository only.
|
|
46
|
+
- Leave an auditable, reviewable result.
|
|
47
|
+
|
|
48
|
+
Context pack:
|
|
49
|
+
${contextPack}
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
async function executeAgent(input) {
|
|
53
|
+
const contextPack = await readFile(input.contextPackPath, "utf8");
|
|
54
|
+
const prompt = renderPrompt(input, contextPack);
|
|
55
|
+
await writeFile(input.promptPath, prompt, "utf8");
|
|
56
|
+
await appendFile(input.executionLogPath, `# Agent: ${input.agent}\n# Command: ${input.command}\n\n`, "utf8");
|
|
57
|
+
const result = await runCommand(input.command, input.cwd, prompt, 10 * 60 * 1000, {
|
|
58
|
+
live: input.live,
|
|
59
|
+
stdoutPrefix: `[${input.agent}] `,
|
|
60
|
+
stderrPrefix: `[${input.agent}:stderr] `
|
|
61
|
+
});
|
|
62
|
+
await appendFile(input.executionLogPath, `## STDOUT\n${result.stdout}\n\n## STDERR\n${result.stderr}\n`, "utf8");
|
|
63
|
+
const changedFilesRaw = await git(input.cwd, "diff --name-only");
|
|
64
|
+
const diff = await git(input.cwd, "diff --no-ext-diff");
|
|
65
|
+
const changedFiles = changedFilesRaw ? changedFilesRaw.split(/\r?\n/).filter(Boolean) : [];
|
|
66
|
+
return {
|
|
67
|
+
status: result.exitCode === 0 ? "success" : "failed",
|
|
68
|
+
exitCode: result.exitCode,
|
|
69
|
+
stdout: truncate(result.stdout, 8000),
|
|
70
|
+
stderr: truncate(result.stderr, 8000),
|
|
71
|
+
changedFiles,
|
|
72
|
+
diff,
|
|
73
|
+
error: result.exitCode === 0 ? undefined : `Agent command failed with exit code ${result.exitCode}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export class CodexAdapter {
|
|
77
|
+
command;
|
|
78
|
+
name = "codex";
|
|
79
|
+
constructor(command) {
|
|
80
|
+
this.command = command;
|
|
81
|
+
}
|
|
82
|
+
run(input) {
|
|
83
|
+
return executeAgent({ ...input, agent: this.name, command: this.command });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export class ClaudeAdapter {
|
|
87
|
+
command;
|
|
88
|
+
name = "claude";
|
|
89
|
+
constructor(command) {
|
|
90
|
+
this.command = command;
|
|
91
|
+
}
|
|
92
|
+
run(input) {
|
|
93
|
+
return executeAgent({ ...input, agent: this.name, command: this.command });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function createAgentAdapter(agent, config) {
|
|
97
|
+
if (agent === "codex")
|
|
98
|
+
return new CodexAdapter(config.agents.codex.command);
|
|
99
|
+
return new ClaudeAdapter(config.agents.claude.command);
|
|
100
|
+
}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { ensureDir, nowIso } from "./utils.js";
|
|
3
|
+
export class AuditLogger {
|
|
4
|
+
auditPath;
|
|
5
|
+
constructor(auditPath) {
|
|
6
|
+
this.auditPath = auditPath;
|
|
7
|
+
}
|
|
8
|
+
async event(event) {
|
|
9
|
+
const line = JSON.stringify({ timestamp: nowIso(), ...event });
|
|
10
|
+
await appendFile(this.auditPath, `${line}\n`, "utf8");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function writeJson(filePath, value) {
|
|
14
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
15
|
+
}
|
|
16
|
+
export async function prepareRunFiles(paths) {
|
|
17
|
+
await ensureDir(paths.runDir);
|
|
18
|
+
await writeJson(paths.intake, {});
|
|
19
|
+
await writeFile(paths.mcpQueries, "", "utf8");
|
|
20
|
+
await writeFile(paths.audit, "", "utf8");
|
|
21
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AuditLogger, prepareRunFiles, writeJson } from "./audit.js";
|
|
5
|
+
import { createAgentAdapter } from "./agent.js";
|
|
6
|
+
import { loadConfig, loadSkill } from "./config.js";
|
|
7
|
+
import { buildContextPack } from "./context.js";
|
|
8
|
+
import { initHarness } from "./init.js";
|
|
9
|
+
import { installIntegration } from "./integration.js";
|
|
10
|
+
import { makeRunPaths } from "./runPaths.js";
|
|
11
|
+
import { writeSummary } from "./summarize.js";
|
|
12
|
+
import { runVerify } from "./verify.js";
|
|
13
|
+
import { git, pathExists } from "./utils.js";
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const [command = "help", ...rest] = argv;
|
|
16
|
+
const args = {};
|
|
17
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
18
|
+
const item = rest[i];
|
|
19
|
+
if (!item.startsWith("--"))
|
|
20
|
+
continue;
|
|
21
|
+
const key = item.slice(2);
|
|
22
|
+
const next = rest[i + 1];
|
|
23
|
+
if (!next || next.startsWith("--"))
|
|
24
|
+
args[key] = true;
|
|
25
|
+
else {
|
|
26
|
+
args[key] = next;
|
|
27
|
+
i += 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { command, args };
|
|
31
|
+
}
|
|
32
|
+
function usage() {
|
|
33
|
+
return `AI Dev Harness
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
harness init
|
|
37
|
+
harness run --task <text> --skill <name> --agent <codex|claude>
|
|
38
|
+
harness run --task <text> --skill <name> --agent <codex|claude> --live
|
|
39
|
+
harness verify --run <run_id_or_path>
|
|
40
|
+
harness summarize --run <run_id_or_path>
|
|
41
|
+
harness install-integration --agent <claude|codex|both> [--target <repo>] [--command <harness command>]
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
function requireString(args, key) {
|
|
45
|
+
const value = args[key];
|
|
46
|
+
if (typeof value !== "string" || !value.trim())
|
|
47
|
+
throw new Error(`Missing required --${key}`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function requireAgent(value) {
|
|
51
|
+
if (value !== "codex" && value !== "claude")
|
|
52
|
+
throw new Error(`Invalid agent: ${value}. Expected codex or claude.`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function quoteCommandPath(filePath) {
|
|
56
|
+
return `"${filePath.replace(/"/g, '\\"')}"`;
|
|
57
|
+
}
|
|
58
|
+
async function resolveRunDir(cwd, runArg) {
|
|
59
|
+
const direct = path.resolve(cwd, runArg);
|
|
60
|
+
if (await pathExists(direct))
|
|
61
|
+
return direct;
|
|
62
|
+
const config = await loadConfig(cwd);
|
|
63
|
+
const underAudit = path.join(cwd, config.audit.path, runArg);
|
|
64
|
+
if (await pathExists(underAudit))
|
|
65
|
+
return underAudit;
|
|
66
|
+
throw new Error(`Run not found: ${runArg}`);
|
|
67
|
+
}
|
|
68
|
+
async function commandInit(cwd) {
|
|
69
|
+
const result = await initHarness(cwd);
|
|
70
|
+
console.log(`Initialized AI Dev Harness in ${cwd}`);
|
|
71
|
+
console.log(`Created ${result.created.length} file(s), skipped ${result.skipped.length} existing file(s).`);
|
|
72
|
+
}
|
|
73
|
+
function sayLive(live, message) {
|
|
74
|
+
if (live)
|
|
75
|
+
console.log(`[harness] ${message}`);
|
|
76
|
+
}
|
|
77
|
+
async function commandRun(cwd, args) {
|
|
78
|
+
const task = requireString(args, "task");
|
|
79
|
+
const skillName = requireString(args, "skill");
|
|
80
|
+
const agentName = requireAgent(requireString(args, "agent"));
|
|
81
|
+
const live = Boolean(args.live);
|
|
82
|
+
const config = await loadConfig(cwd);
|
|
83
|
+
const skill = await loadSkill(cwd, skillName);
|
|
84
|
+
const paths = makeRunPaths(cwd, config, task);
|
|
85
|
+
await prepareRunFiles(paths);
|
|
86
|
+
const audit = new AuditLogger(paths.audit);
|
|
87
|
+
sayLive(live, `run started: skill=${skillName} agent=${agentName}`);
|
|
88
|
+
sayLive(live, `evidence directory: ${paths.runDir}`);
|
|
89
|
+
await audit.event({ stage: "intake", event: "run_started", status: "started", detail: { task, skill: skillName, agent: agentName } });
|
|
90
|
+
const isGitRepo = Boolean(await git(cwd, "rev-parse --show-toplevel"));
|
|
91
|
+
const intake = {
|
|
92
|
+
task,
|
|
93
|
+
skill: skillName,
|
|
94
|
+
agent: agentName,
|
|
95
|
+
cwd,
|
|
96
|
+
isGitRepo,
|
|
97
|
+
gitStatus: await git(cwd, "status --short"),
|
|
98
|
+
startedAt: new Date().toISOString()
|
|
99
|
+
};
|
|
100
|
+
await writeJson(paths.intake, intake);
|
|
101
|
+
if (!isGitRepo) {
|
|
102
|
+
await audit.event({
|
|
103
|
+
stage: "intake",
|
|
104
|
+
event: "git_repo_check",
|
|
105
|
+
status: "failed",
|
|
106
|
+
detail: "Current directory is not a Git repository. Run inside a business Git repository before executing harness run."
|
|
107
|
+
});
|
|
108
|
+
await audit.event({ stage: "run", event: "run_finished", status: "failed" });
|
|
109
|
+
throw new Error(`harness run must execute inside a Git repository. Evidence: ${paths.runDir}`);
|
|
110
|
+
}
|
|
111
|
+
sayLive(live, "building context pack with MCP first, local fallback second");
|
|
112
|
+
await audit.event({ stage: "context_pack", event: "build_started", status: "started" });
|
|
113
|
+
const contextPack = await buildContextPack({
|
|
114
|
+
cwd,
|
|
115
|
+
task,
|
|
116
|
+
config,
|
|
117
|
+
skill,
|
|
118
|
+
contextPackPath: paths.contextPack,
|
|
119
|
+
mcpLogPath: paths.mcpQueries,
|
|
120
|
+
audit
|
|
121
|
+
});
|
|
122
|
+
await audit.event({
|
|
123
|
+
stage: "context_pack",
|
|
124
|
+
event: "build_finished",
|
|
125
|
+
status: contextPack.degraded ? "degraded" : "succeeded"
|
|
126
|
+
});
|
|
127
|
+
sayLive(live, `context pack ${contextPack.degraded ? "degraded" : "ready"}: ${paths.contextPack}`);
|
|
128
|
+
const plan = `# Execution Plan
|
|
129
|
+
|
|
130
|
+
The selected agent must:
|
|
131
|
+
1. Read the context pack.
|
|
132
|
+
2. Produce impact analysis.
|
|
133
|
+
3. Make the smallest safe change.
|
|
134
|
+
4. Leave enough evidence for review.
|
|
135
|
+
5. Allow the harness to run verification and summarize results.
|
|
136
|
+
`;
|
|
137
|
+
await writeFile(paths.plan, plan, "utf8");
|
|
138
|
+
await audit.event({ stage: "plan", event: "plan_written", status: "succeeded", detail: { path: paths.plan } });
|
|
139
|
+
sayLive(live, `plan written: ${paths.plan}`);
|
|
140
|
+
const adapter = createAgentAdapter(agentName, config);
|
|
141
|
+
const agentCommand = agentName === "codex" ? config.agents.codex.command : config.agents.claude.command;
|
|
142
|
+
sayLive(live, `executing ${adapter.name}: ${agentCommand}`);
|
|
143
|
+
await audit.event({ stage: "execute", event: "agent_started", status: "started", detail: { agent: adapter.name, command: agentCommand } });
|
|
144
|
+
let agentResult;
|
|
145
|
+
try {
|
|
146
|
+
agentResult = await adapter.run({
|
|
147
|
+
cwd,
|
|
148
|
+
task,
|
|
149
|
+
skill,
|
|
150
|
+
contextPackPath: paths.contextPack,
|
|
151
|
+
promptPath: paths.agentPrompt,
|
|
152
|
+
executionLogPath: paths.executionLog,
|
|
153
|
+
live
|
|
154
|
+
});
|
|
155
|
+
await audit.event({
|
|
156
|
+
stage: "execute",
|
|
157
|
+
event: "agent_finished",
|
|
158
|
+
status: agentResult.status === "success" ? "succeeded" : "failed",
|
|
159
|
+
detail: { exitCode: agentResult.exitCode, changedFiles: agentResult.changedFiles, error: agentResult.error }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
agentResult = {
|
|
164
|
+
status: "failed",
|
|
165
|
+
exitCode: null,
|
|
166
|
+
stdout: "",
|
|
167
|
+
stderr: "",
|
|
168
|
+
changedFiles: [],
|
|
169
|
+
diff: "",
|
|
170
|
+
error: error instanceof Error ? error.message : String(error)
|
|
171
|
+
};
|
|
172
|
+
await audit.event({ stage: "execute", event: "agent_error", status: "failed", detail: agentResult.error });
|
|
173
|
+
}
|
|
174
|
+
await writeFile(paths.gitDiff, agentResult.diff, "utf8");
|
|
175
|
+
sayLive(live, `agent finished: status=${agentResult.status} changedFiles=${agentResult.changedFiles.length}`);
|
|
176
|
+
sayLive(live, "running verification commands");
|
|
177
|
+
await audit.event({ stage: "verify", event: "verify_started", status: "started" });
|
|
178
|
+
const verifyResult = await runVerify(cwd, config, skill, paths.verify);
|
|
179
|
+
await audit.event({ stage: "verify", event: "verify_finished", status: verifyResult.status === "passed" ? "succeeded" : "failed", detail: verifyResult });
|
|
180
|
+
sayLive(live, `verification ${verifyResult.status}: ${paths.verify}`);
|
|
181
|
+
await writeSummary({
|
|
182
|
+
task,
|
|
183
|
+
skillName,
|
|
184
|
+
agentName,
|
|
185
|
+
agentResult,
|
|
186
|
+
verifyResult,
|
|
187
|
+
gitDiffPath: paths.gitDiff,
|
|
188
|
+
summaryPath: paths.summary,
|
|
189
|
+
persistPath: paths.persistSuggestions
|
|
190
|
+
});
|
|
191
|
+
await audit.event({ stage: "summarize", event: "summary_written", status: "succeeded" });
|
|
192
|
+
await audit.event({ stage: "persist", event: "suggestions_written", status: "succeeded" });
|
|
193
|
+
sayLive(live, `summary written: ${paths.summary}`);
|
|
194
|
+
sayLive(live, `persist suggestions written: ${paths.persistSuggestions}`);
|
|
195
|
+
const runPassed = verifyResult.status === "passed" && agentResult.status === "success";
|
|
196
|
+
await audit.event({ stage: "run", event: "run_finished", status: runPassed ? "succeeded" : "failed" });
|
|
197
|
+
console.log(`Run complete: ${paths.runId}`);
|
|
198
|
+
console.log(`Evidence: ${paths.runDir}`);
|
|
199
|
+
if (!runPassed)
|
|
200
|
+
throw new Error(`Harness run failed. Evidence: ${paths.runDir}`);
|
|
201
|
+
}
|
|
202
|
+
async function commandInstallIntegration(cwd, args) {
|
|
203
|
+
const rawAgent = String(args.agent ?? "both");
|
|
204
|
+
if (rawAgent !== "claude" && rawAgent !== "codex" && rawAgent !== "both") {
|
|
205
|
+
throw new Error(`Invalid --agent ${rawAgent}. Expected claude, codex, or both.`);
|
|
206
|
+
}
|
|
207
|
+
const targetDir = typeof args.target === "string" ? path.resolve(cwd, args.target) : cwd;
|
|
208
|
+
const packageRoot = path.resolve(path.dirname(process.argv[1]), "..");
|
|
209
|
+
const harnessCommand = typeof args.command === "string" ? args.command : `node ${quoteCommandPath(path.resolve(process.argv[1]))}`;
|
|
210
|
+
const result = await installIntegration({ targetDir, harnessCommand, agent: rawAgent, packageRoot });
|
|
211
|
+
console.log(`Installed AI Dev Harness conversation integration in ${targetDir}`);
|
|
212
|
+
console.log(`Created: ${result.created.length ? result.created.join(", ") : "(none)"}`);
|
|
213
|
+
console.log(`Updated: ${result.updated.length ? result.updated.join(", ") : "(none)"}`);
|
|
214
|
+
console.log(`Skipped: ${result.skipped.length ? result.skipped.join(", ") : "(none)"}`);
|
|
215
|
+
}
|
|
216
|
+
async function commandVerify(cwd, args) {
|
|
217
|
+
const runDir = await resolveRunDir(cwd, requireString(args, "run"));
|
|
218
|
+
const intake = JSON.parse(await readFile(path.join(runDir, "intake.json"), "utf8"));
|
|
219
|
+
const config = await loadConfig(cwd);
|
|
220
|
+
const skill = await loadSkill(cwd, intake.skill);
|
|
221
|
+
const result = await runVerify(cwd, config, skill, path.join(runDir, "verify.json"));
|
|
222
|
+
const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
|
|
223
|
+
await audit.event({ stage: "verify", event: "verify_rerun", status: result.status === "passed" ? "succeeded" : "failed", detail: result });
|
|
224
|
+
console.log(`Verify ${result.status}: ${runDir}`);
|
|
225
|
+
}
|
|
226
|
+
async function commandSummarize(cwd, args) {
|
|
227
|
+
const runDir = await resolveRunDir(cwd, requireString(args, "run"));
|
|
228
|
+
const intake = JSON.parse(await readFile(path.join(runDir, "intake.json"), "utf8"));
|
|
229
|
+
const verifyPath = path.join(runDir, "verify.json");
|
|
230
|
+
const verifyResult = (await pathExists(verifyPath))
|
|
231
|
+
? JSON.parse(await readFile(verifyPath, "utf8"))
|
|
232
|
+
: undefined;
|
|
233
|
+
const changedFilesRaw = await git(cwd, "diff --name-only");
|
|
234
|
+
const agentResult = {
|
|
235
|
+
status: "success",
|
|
236
|
+
exitCode: 0,
|
|
237
|
+
stdout: "",
|
|
238
|
+
stderr: "",
|
|
239
|
+
changedFiles: changedFilesRaw ? changedFilesRaw.split(/\r?\n/).filter(Boolean) : [],
|
|
240
|
+
diff: await git(cwd, "diff --no-ext-diff")
|
|
241
|
+
};
|
|
242
|
+
await writeFile(path.join(runDir, "git_diff.patch"), agentResult.diff, "utf8");
|
|
243
|
+
await writeSummary({
|
|
244
|
+
task: intake.task,
|
|
245
|
+
skillName: intake.skill,
|
|
246
|
+
agentName: intake.agent,
|
|
247
|
+
agentResult,
|
|
248
|
+
verifyResult,
|
|
249
|
+
gitDiffPath: path.join(runDir, "git_diff.patch"),
|
|
250
|
+
summaryPath: path.join(runDir, "summary.md"),
|
|
251
|
+
persistPath: path.join(runDir, "persist_suggestions.md")
|
|
252
|
+
});
|
|
253
|
+
const audit = new AuditLogger(path.join(runDir, "audit.jsonl"));
|
|
254
|
+
await audit.event({ stage: "summarize", event: "summary_rerun", status: "succeeded" });
|
|
255
|
+
console.log(`Summary refreshed: ${runDir}`);
|
|
256
|
+
}
|
|
257
|
+
async function main() {
|
|
258
|
+
const cwd = process.cwd();
|
|
259
|
+
const { command, args } = parseArgs(process.argv.slice(2));
|
|
260
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
261
|
+
console.log(usage());
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (command === "init")
|
|
265
|
+
return commandInit(cwd);
|
|
266
|
+
if (command === "run")
|
|
267
|
+
return commandRun(cwd, args);
|
|
268
|
+
if (command === "verify")
|
|
269
|
+
return commandVerify(cwd, args);
|
|
270
|
+
if (command === "summarize")
|
|
271
|
+
return commandSummarize(cwd, args);
|
|
272
|
+
if (command === "install-integration")
|
|
273
|
+
return commandInstallIntegration(cwd, args);
|
|
274
|
+
throw new Error(`Unknown command: ${command}\n${usage()}`);
|
|
275
|
+
}
|
|
276
|
+
main().catch((error) => {
|
|
277
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { pathExists, readTextIfExists } from "./utils.js";
|
|
4
|
+
export const defaultConfig = {
|
|
5
|
+
agents: {
|
|
6
|
+
codex: { command: "codex exec --full-auto -" },
|
|
7
|
+
claude: { command: "claude -p -" }
|
|
8
|
+
},
|
|
9
|
+
mcp: {
|
|
10
|
+
codegraph: { url: "http://localhost:7331/mcp" },
|
|
11
|
+
knowledgeGraph: { url: "http://localhost:7332/mcp" },
|
|
12
|
+
timeoutMs: 15000
|
|
13
|
+
},
|
|
14
|
+
verify: {
|
|
15
|
+
defaultCommands: [{ name: "git status", command: "git status --short" }]
|
|
16
|
+
},
|
|
17
|
+
audit: { path: "runs" },
|
|
18
|
+
context: {
|
|
19
|
+
fallbackInclude: ["AGENTS.md", "TOOLS.md", "SKILLS.md", "README.md", "package.json", "src"],
|
|
20
|
+
exclude: ["node_modules", ".git", "dist", "build", ".next", "coverage", "runs"]
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export function renderYaml(value, indent = 0) {
|
|
24
|
+
const pad = " ".repeat(indent);
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value
|
|
27
|
+
.map((item) => {
|
|
28
|
+
if (typeof item === "object" && item !== null) {
|
|
29
|
+
const entries = Object.entries(item);
|
|
30
|
+
if (!entries.length)
|
|
31
|
+
return `${pad}- {}`;
|
|
32
|
+
const [firstKey, firstValue] = entries[0];
|
|
33
|
+
const first = typeof firstValue === "object" && firstValue !== null
|
|
34
|
+
? `${pad}- ${firstKey}:\n${renderYaml(firstValue, indent + 4)}`
|
|
35
|
+
: `${pad}- ${firstKey}: ${String(firstValue)}`;
|
|
36
|
+
const rest = entries
|
|
37
|
+
.slice(1)
|
|
38
|
+
.map(([key, child]) => {
|
|
39
|
+
if (typeof child === "object" && child !== null)
|
|
40
|
+
return `${pad} ${key}:\n${renderYaml(child, indent + 4)}`;
|
|
41
|
+
return `${pad} ${key}: ${String(child)}`;
|
|
42
|
+
})
|
|
43
|
+
.join("\n");
|
|
44
|
+
return rest ? `${first}\n${rest}` : first;
|
|
45
|
+
}
|
|
46
|
+
return `${pad}- ${String(item)}`;
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "object" && value !== null) {
|
|
51
|
+
return Object.entries(value)
|
|
52
|
+
.map(([key, item]) => {
|
|
53
|
+
if (typeof item === "object" && item !== null)
|
|
54
|
+
return `${pad}${key}:\n${renderYaml(item, indent + 2)}`;
|
|
55
|
+
return `${pad}${key}: ${String(item)}`;
|
|
56
|
+
})
|
|
57
|
+
.join("\n");
|
|
58
|
+
}
|
|
59
|
+
return `${pad}${String(value)}`;
|
|
60
|
+
}
|
|
61
|
+
export function defaultConfigYaml() {
|
|
62
|
+
return `${renderYaml(defaultConfig)}\n`;
|
|
63
|
+
}
|
|
64
|
+
function readScalar(raw) {
|
|
65
|
+
const value = raw.trim().replace(/^["']|["']$/g, "");
|
|
66
|
+
if (/^\d+$/.test(value))
|
|
67
|
+
return Number(value);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function parseVerifyCommands(text) {
|
|
71
|
+
const commands = [];
|
|
72
|
+
const itemRegex = /-\s*name:\s*([^\n]+)\n\s+command:\s*([^\n]+)/g;
|
|
73
|
+
for (const match of text.matchAll(itemRegex)) {
|
|
74
|
+
commands.push({ name: String(readScalar(match[1])), command: String(readScalar(match[2])) });
|
|
75
|
+
}
|
|
76
|
+
const simpleRegex = /-\s+([^\n:]+)$/gm;
|
|
77
|
+
if (commands.length === 0) {
|
|
78
|
+
for (const match of text.matchAll(simpleRegex))
|
|
79
|
+
commands.push({ name: match[1].trim(), command: match[1].trim() });
|
|
80
|
+
}
|
|
81
|
+
return commands;
|
|
82
|
+
}
|
|
83
|
+
function parseStringList(text) {
|
|
84
|
+
return [...text.matchAll(/-\s+(.+)$/gm)].map((m) => String(readScalar(m[1])));
|
|
85
|
+
}
|
|
86
|
+
function topSection(text, key) {
|
|
87
|
+
const match = text.match(new RegExp(`^${key}:\\n([\\s\\S]*?)(?=^[a-zA-Z][\\w]*:|$)`, "m"));
|
|
88
|
+
return match?.[1] ?? "";
|
|
89
|
+
}
|
|
90
|
+
function scalar(text, regex, fallback) {
|
|
91
|
+
const match = text.match(regex);
|
|
92
|
+
return match ? readScalar(match[1]) : fallback;
|
|
93
|
+
}
|
|
94
|
+
function blockList(text, key) {
|
|
95
|
+
const match = text.match(new RegExp(`^\\s*${key}:\\n([\\s\\S]*?)(?=^\\s*[a-zA-Z][\\w]*:|^\\S|$)`, "m"));
|
|
96
|
+
return match ? parseStringList(match[1]) : [];
|
|
97
|
+
}
|
|
98
|
+
export async function loadConfig(cwd) {
|
|
99
|
+
const configPath = path.join(cwd, "harness.yaml");
|
|
100
|
+
if (!(await pathExists(configPath)))
|
|
101
|
+
return defaultConfig;
|
|
102
|
+
const text = await readFile(configPath, "utf8");
|
|
103
|
+
const verifyRoot = topSection(text, "verify");
|
|
104
|
+
const verifySection = verifyRoot.match(/defaultCommands:\n([\s\S]*?)(?=^\S|^ [a-zA-Z][\w]*:|$)/m)?.[1] ?? "";
|
|
105
|
+
const contextSection = topSection(text, "context");
|
|
106
|
+
return {
|
|
107
|
+
agents: {
|
|
108
|
+
codex: {
|
|
109
|
+
command: String(scalar(text, /agents:\n[\s\S]*?codex:\n\s+command:\s*([^\n]+)/m, defaultConfig.agents.codex.command))
|
|
110
|
+
},
|
|
111
|
+
claude: {
|
|
112
|
+
command: String(scalar(text, /agents:\n[\s\S]*?claude:\n\s+command:\s*([^\n]+)/m, defaultConfig.agents.claude.command))
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
mcp: {
|
|
116
|
+
codegraph: {
|
|
117
|
+
url: String(scalar(text, /mcp:\n[\s\S]*?codegraph:\n\s+url:\s*([^\n]+)/m, defaultConfig.mcp.codegraph.url))
|
|
118
|
+
},
|
|
119
|
+
knowledgeGraph: {
|
|
120
|
+
url: String(scalar(text, /mcp:\n[\s\S]*?knowledgeGraph:\n\s+url:\s*([^\n]+)/m, defaultConfig.mcp.knowledgeGraph.url))
|
|
121
|
+
},
|
|
122
|
+
timeoutMs: Number(scalar(text, /mcp:\n[\s\S]*?timeoutMs:\s*([^\n]+)/m, defaultConfig.mcp.timeoutMs))
|
|
123
|
+
},
|
|
124
|
+
verify: {
|
|
125
|
+
defaultCommands: parseVerifyCommands(verifySection).length
|
|
126
|
+
? parseVerifyCommands(verifySection)
|
|
127
|
+
: defaultConfig.verify.defaultCommands
|
|
128
|
+
},
|
|
129
|
+
audit: { path: String(scalar(text, /audit:\n\s+path:\s*([^\n]+)/m, defaultConfig.audit.path)) },
|
|
130
|
+
context: {
|
|
131
|
+
fallbackInclude: blockList(contextSection, "fallbackInclude").length
|
|
132
|
+
? blockList(contextSection, "fallbackInclude")
|
|
133
|
+
: defaultConfig.context.fallbackInclude,
|
|
134
|
+
exclude: blockList(contextSection, "exclude").length
|
|
135
|
+
? blockList(contextSection, "exclude")
|
|
136
|
+
: defaultConfig.context.exclude
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export async function loadSkill(cwd, name) {
|
|
141
|
+
const dir = path.join(cwd, "skills", name);
|
|
142
|
+
if (!(await pathExists(path.join(dir, "SKILL.md")))) {
|
|
143
|
+
throw new Error(`Skill not found: ${name}. Expected ${path.join(dir, "SKILL.md")}`);
|
|
144
|
+
}
|
|
145
|
+
const markdown = await readTextIfExists(path.join(dir, "SKILL.md"));
|
|
146
|
+
const verifyText = await readTextIfExists(path.join(dir, "verify.yaml"));
|
|
147
|
+
const contextText = await readTextIfExists(path.join(dir, "context.yaml"));
|
|
148
|
+
return {
|
|
149
|
+
name,
|
|
150
|
+
dir,
|
|
151
|
+
markdown,
|
|
152
|
+
verifyCommands: parseVerifyCommands(verifyText),
|
|
153
|
+
contextQueries: blockList(contextText, "queries"),
|
|
154
|
+
fallbackInclude: blockList(contextText, "fallbackInclude")
|
|
155
|
+
};
|
|
156
|
+
}
|