@cardor/agent-harness-kit 0.14.0 → 0.15.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/bin/ahk.js +1 -1
- package/dist/chunk-LQ7SDMK6.js +82 -0
- package/dist/chunk-LQ7SDMK6.js.map +1 -0
- package/dist/cli.js +1984 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -6
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/commands/build.d.ts +0 -6
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js +0 -39
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -7
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -27
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.d.ts.map +0 -1
- package/dist/commands/export.js +0 -33
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/health.d.ts +0 -2
- package/dist/commands/health.d.ts.map +0 -1
- package/dist/commands/health.js +0 -78
- package/dist/commands/health.js.map +0 -1
- package/dist/commands/init-helpers.d.ts +0 -9
- package/dist/commands/init-helpers.d.ts.map +0 -1
- package/dist/commands/init-helpers.js +0 -40
- package/dist/commands/init-helpers.js.map +0 -1
- package/dist/commands/init.d.ts +0 -9
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -192
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/migrate.d.ts +0 -6
- package/dist/commands/migrate.d.ts.map +0 -1
- package/dist/commands/migrate.js +0 -45
- package/dist/commands/migrate.js.map +0 -1
- package/dist/commands/serve.d.ts +0 -6
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.js +0 -13
- package/dist/commands/serve.js.map +0 -1
- package/dist/commands/status.d.ts +0 -6
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -71
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -7
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -57
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/task/add.d.ts +0 -2
- package/dist/commands/task/add.d.ts.map +0 -1
- package/dist/commands/task/add.js +0 -53
- package/dist/commands/task/add.js.map +0 -1
- package/dist/commands/task/done.d.ts +0 -2
- package/dist/commands/task/done.d.ts.map +0 -1
- package/dist/commands/task/done.js +0 -45
- package/dist/commands/task/done.js.map +0 -1
- package/dist/commands/task/index.d.ts +0 -4
- package/dist/commands/task/index.d.ts.map +0 -1
- package/dist/commands/task/index.js +0 -4
- package/dist/commands/task/index.js.map +0 -1
- package/dist/commands/task/list.d.ts +0 -7
- package/dist/commands/task/list.d.ts.map +0 -1
- package/dist/commands/task/list.js +0 -42
- package/dist/commands/task/list.js.map +0 -1
- package/dist/core/config.d.ts +0 -5
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -77
- package/dist/core/config.js.map +0 -1
- package/dist/core/dashboard-server.d.ts +0 -7
- package/dist/core/dashboard-server.d.ts.map +0 -1
- package/dist/core/dashboard-server.js +0 -223
- package/dist/core/dashboard-server.js.map +0 -1
- package/dist/core/db.d.ts +0 -57
- package/dist/core/db.d.ts.map +0 -1
- package/dist/core/db.js +0 -348
- package/dist/core/db.js.map +0 -1
- package/dist/core/materializer/agent-templates/test123.txt +0 -0
- package/dist/core/materializer/claude-code.d.ts +0 -8
- package/dist/core/materializer/claude-code.d.ts.map +0 -1
- package/dist/core/materializer/claude-code.js +0 -66
- package/dist/core/materializer/claude-code.js.map +0 -1
- package/dist/core/materializer/index.d.ts +0 -8
- package/dist/core/materializer/index.d.ts.map +0 -1
- package/dist/core/materializer/index.js +0 -13
- package/dist/core/materializer/index.js.map +0 -1
- package/dist/core/materializer/mcp-merge.d.ts +0 -3
- package/dist/core/materializer/mcp-merge.d.ts.map +0 -1
- package/dist/core/materializer/mcp-merge.js +0 -59
- package/dist/core/materializer/mcp-merge.js.map +0 -1
- package/dist/core/materializer/opencode.d.ts +0 -8
- package/dist/core/materializer/opencode.d.ts.map +0 -1
- package/dist/core/materializer/opencode.js +0 -59
- package/dist/core/materializer/opencode.js.map +0 -1
- package/dist/core/materializer/scaffold-utils.d.ts +0 -4
- package/dist/core/materializer/scaffold-utils.d.ts.map +0 -1
- package/dist/core/materializer/scaffold-utils.js +0 -28
- package/dist/core/materializer/scaffold-utils.js.map +0 -1
- package/dist/core/materializer/templates.d.ts +0 -33
- package/dist/core/materializer/templates.d.ts.map +0 -1
- package/dist/core/materializer/templates.js +0 -187
- package/dist/core/materializer/templates.js.map +0 -1
- package/dist/core/mcp-server.d.ts +0 -3
- package/dist/core/mcp-server.d.ts.map +0 -1
- package/dist/core/mcp-server.js +0 -264
- package/dist/core/mcp-server.js.map +0 -1
- package/dist/core/server-types.d.ts +0 -67
- package/dist/core/server-types.d.ts.map +0 -1
- package/dist/core/server-types.js +0 -3
- package/dist/core/server-types.js.map +0 -1
- package/dist/core/sqlite-adapter.d.ts +0 -14
- package/dist/core/sqlite-adapter.d.ts.map +0 -1
- package/dist/core/sqlite-adapter.js +0 -20
- package/dist/core/sqlite-adapter.js.map +0 -1
- package/dist/dashboard-dist/assets/index-Cdm4QZ8j.js +0 -9
- package/dist/dashboard-dist/assets/index-DJInh0UZ.js +0 -9
- package/dist/dashboard-dist/assets/index-TQMzdmXs.css +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/tests/db.test.d.ts +0 -2
- package/dist/tests/db.test.d.ts.map +0 -1
- package/dist/tests/db.test.js +0 -172
- package/dist/tests/db.test.js.map +0 -1
- package/dist/tests/slugify.test.d.ts +0 -2
- package/dist/tests/slugify.test.d.ts.map +0 -1
- package/dist/tests/slugify.test.js +0 -19
- package/dist/tests/slugify.test.js.map +0 -1
- package/dist/tests/templates.test.d.ts +0 -2
- package/dist/tests/templates.test.d.ts.map +0 -1
- package/dist/tests/templates.test.js +0 -71
- package/dist/tests/templates.test.js.map +0 -1
- package/dist/types.d.ts +0 -141
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,123 +1,1998 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "./chunk-LQ7SDMK6.js";
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands/build.ts
|
|
9
|
+
import { watch } from "fs";
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
|
|
13
|
+
// src/core/materializer/claude-code.ts
|
|
14
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
15
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
16
|
+
|
|
17
|
+
// src/core/materializer/mcp-merge.ts
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
19
|
+
import { dirname } from "path";
|
|
20
|
+
function mergeClaudeMcpJson(filePath, port) {
|
|
21
|
+
const folderPath = dirname(filePath);
|
|
22
|
+
if (!existsSync(folderPath)) {
|
|
23
|
+
mkdirSync(folderPath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
let existing = {};
|
|
26
|
+
if (existsSync(filePath)) {
|
|
27
|
+
try {
|
|
28
|
+
existing = JSON.parse(readFileSync(filePath, "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const merged = {
|
|
33
|
+
...existing,
|
|
34
|
+
mcpServers: {
|
|
35
|
+
...existing.mcpServers ?? {},
|
|
36
|
+
"agent-harness-kit": {
|
|
37
|
+
type: "stdio",
|
|
38
|
+
command: "npx",
|
|
39
|
+
args: ["ahk", "serve", "--port", String(port)]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
44
|
+
writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
45
|
+
}
|
|
46
|
+
function mergeOpencodeJson(filePath, port) {
|
|
47
|
+
const folderPath = dirname(filePath);
|
|
48
|
+
if (!existsSync(folderPath)) {
|
|
49
|
+
mkdirSync(folderPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
let existing = {};
|
|
52
|
+
if (existsSync(filePath)) {
|
|
53
|
+
try {
|
|
54
|
+
existing = JSON.parse(readFileSync(filePath, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const existingMcp = existing.mcp ?? {};
|
|
59
|
+
const merged = {
|
|
60
|
+
...existing,
|
|
61
|
+
mcp: {
|
|
62
|
+
...existingMcp,
|
|
63
|
+
"agent-harness-kit": {
|
|
64
|
+
enabled: true,
|
|
65
|
+
type: "local",
|
|
66
|
+
command: ["npx", "ahk", "serve", "--port", String(port)]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/core/materializer/scaffold-utils.ts
|
|
74
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
75
|
+
import { join as join2, resolve } from "path";
|
|
76
|
+
|
|
77
|
+
// src/core/materializer/templates.ts
|
|
78
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
79
|
+
import { dirname as dirname2, join } from "path";
|
|
80
|
+
import { fileURLToPath } from "url";
|
|
81
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
82
|
+
var TEMPLATES_DIR = join(__dirname, "agent-templates");
|
|
83
|
+
function loadAgentTemplate(name, vars = {}) {
|
|
84
|
+
const raw = readFileSync2(join(TEMPLATES_DIR, `${name}.md`), "utf8");
|
|
85
|
+
return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
86
|
+
}
|
|
87
|
+
var HEALTH_SH = `#!/usr/bin/env bash
|
|
88
|
+
# health.sh \u2014 project health check for agent-harness-kit
|
|
89
|
+
#
|
|
90
|
+
# This script must exit 0 when the project is healthy.
|
|
91
|
+
# Agents will run this before starting work.
|
|
92
|
+
#
|
|
93
|
+
# TODO: implement your project's health checks below.
|
|
94
|
+
# Examples:
|
|
95
|
+
# npm test
|
|
96
|
+
# docker compose ps | grep -q "running"
|
|
97
|
+
# psql -c "SELECT 1" > /dev/null 2>&1
|
|
98
|
+
#
|
|
99
|
+
# Until you implement it, this script intentionally exits 1
|
|
100
|
+
# so agents know the environment is not verified.
|
|
101
|
+
|
|
102
|
+
echo "health.sh not implemented yet."
|
|
103
|
+
echo "Edit this file with your project's health checks."
|
|
104
|
+
echo "It must exit 0 for agents to start working."
|
|
105
|
+
exit 1
|
|
106
|
+
`;
|
|
107
|
+
function agentsMd(config) {
|
|
108
|
+
const { name, description, docsPath } = config.project;
|
|
109
|
+
const port = config.tools.mcp.port;
|
|
110
|
+
return `# AGENTS.md \u2014 ${name}
|
|
111
|
+
|
|
112
|
+
> **Read this file first.** It is the navigation map for every AI agent working in this repository.
|
|
113
|
+
|
|
114
|
+
## Project
|
|
115
|
+
|
|
116
|
+
**${name}** \u2014 ${description}
|
|
117
|
+
|
|
118
|
+
## Health check (run before starting)
|
|
119
|
+
|
|
120
|
+
\`\`\`bash
|
|
121
|
+
bash health.sh
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
If it exits non-zero, stop and report the issue. Do not proceed with tasks until health is green.
|
|
125
|
+
|
|
126
|
+
## Harness data (source of truth)
|
|
127
|
+
|
|
128
|
+
| File | Purpose |
|
|
129
|
+
|------|---------|
|
|
130
|
+
| \`.harness/harness.db\` | SQLite: all tasks, actions, file changes, tool calls |
|
|
131
|
+
| \`.harness/current.md\` | Markdown fallback \u2014 read this if MCP server is unavailable |
|
|
132
|
+
| \`.harness/feature_list.json\` | Human-editable task seed list |
|
|
133
|
+
|
|
134
|
+
## MCP tools (preferred)
|
|
135
|
+
|
|
136
|
+
The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
|
|
137
|
+
|
|
138
|
+
\`\`\`
|
|
139
|
+
actions.start taskId agent \u2192 start an action, returns actionId
|
|
140
|
+
actions.write actionId section text \u2192 record a section (result, tools_used, ...)
|
|
141
|
+
actions.complete actionId summary \u2192 close the action
|
|
142
|
+
actions.get taskId \u2192 full action history for a task
|
|
143
|
+
tasks.get [status] \u2192 list tasks (pending | in_progress | done | blocked)
|
|
144
|
+
tasks.claim id \u2192 atomically claim a pending task
|
|
145
|
+
tasks.update id status \u2192 change task status
|
|
146
|
+
docs.search query \u2192 search ${docsPath} for relevant content
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Workflow
|
|
150
|
+
|
|
151
|
+
\`\`\`
|
|
152
|
+
1. INIT
|
|
153
|
+
- Run health.sh \u2192 exit 1 means stop
|
|
154
|
+
- tasks.get('in_progress') \u2192 resume if something is in progress
|
|
155
|
+
- tasks.get('pending') \u2192 pick lowest id
|
|
156
|
+
|
|
157
|
+
2. WORK (lead \u2192 explorer \u2192 builder \u2192 reviewer)
|
|
158
|
+
- Each agent calls actions.start(taskId, agentName) \u2192 actionId
|
|
159
|
+
- Records work with actions.write(actionId, section, content)
|
|
160
|
+
- Closes with actions.complete(actionId, summary)
|
|
161
|
+
|
|
162
|
+
3. CLOSE
|
|
163
|
+
- tasks.update(taskId, 'done')
|
|
164
|
+
- Run health.sh \u2192 must be green before closing
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
## Agent roles
|
|
168
|
+
|
|
169
|
+
| Agent | Responsibility |
|
|
170
|
+
|-------|---------------|
|
|
171
|
+
| lead | Decomposes the task into a plan, assigns sub-agents |
|
|
172
|
+
| explorer | Reads and maps relevant code, never writes |
|
|
173
|
+
| builder | Implements the plan, writes files |
|
|
174
|
+
| reviewer | Verifies acceptance criteria, approves or blocks |
|
|
175
|
+
|
|
176
|
+
## What to read
|
|
177
|
+
|
|
178
|
+
\`\`\`
|
|
179
|
+
Always: .harness/current.md (or MCP tasks.get)
|
|
180
|
+
If implementing: ${docsPath}/
|
|
181
|
+
If orchestrating: Agent definition files in your provider's agents directory
|
|
182
|
+
\`\`\`
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
function configTs(params) {
|
|
186
|
+
return `import { defineHarness } from '@cardor/agent-harness-kit'
|
|
187
|
+
|
|
188
|
+
export default defineHarness({
|
|
189
|
+
project: {
|
|
190
|
+
name: '${params.name}',
|
|
191
|
+
description: '${params.description}',
|
|
192
|
+
docsPath: '${params.docsPath}',
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
provider: '${params.provider}',
|
|
196
|
+
|
|
197
|
+
agents: {
|
|
198
|
+
lead: { instructionsPath: null },
|
|
199
|
+
explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src'] },
|
|
200
|
+
builder: { instructionsPath: null, writablePaths: ['./src', './tests'] },
|
|
201
|
+
reviewer: { instructionsPath: null },
|
|
202
|
+
custom: [],
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
storage: {
|
|
206
|
+
dir: '.harness',
|
|
207
|
+
dbPath: '.harness/harness.db',
|
|
208
|
+
tasks: { adapter: '${params.tasksAdapter}' },
|
|
209
|
+
sections: {
|
|
210
|
+
toolsUsed: true,
|
|
211
|
+
filesModified: true,
|
|
212
|
+
result: true,
|
|
213
|
+
blockers: true,
|
|
214
|
+
nextSteps: false,
|
|
215
|
+
},
|
|
216
|
+
markdownFallback: { enabled: true, path: '.harness/current.md' },
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
health: {
|
|
220
|
+
scriptPath: './health.sh',
|
|
221
|
+
required: true,
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
tools: {
|
|
225
|
+
mcp: { enabled: true, port: ${params.port} },
|
|
226
|
+
scripts: { enabled: true, outputDir: './.harness/scripts' },
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
function agentLead(vars) {
|
|
232
|
+
return loadAgentTemplate("lead", vars);
|
|
233
|
+
}
|
|
234
|
+
function agentExplorer(vars) {
|
|
235
|
+
return loadAgentTemplate("explorer", vars);
|
|
236
|
+
}
|
|
237
|
+
function agentBuilder(vars) {
|
|
238
|
+
return loadAgentTemplate("builder", vars);
|
|
239
|
+
}
|
|
240
|
+
function agentReviewer(vars) {
|
|
241
|
+
return loadAgentTemplate("reviewer", vars);
|
|
242
|
+
}
|
|
243
|
+
function featureListJson(tasks) {
|
|
244
|
+
return JSON.stringify(tasks, null, 2) + "\n";
|
|
245
|
+
}
|
|
246
|
+
var GITIGNORE_ENTRIES = `
|
|
247
|
+
# agent-harness-kit
|
|
248
|
+
.harness/harness.db
|
|
249
|
+
.harness/harness.db-shm
|
|
250
|
+
.harness/harness.db-wal
|
|
251
|
+
.harness/current.md
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
// src/core/materializer/scaffold-utils.ts
|
|
255
|
+
function writeAgentFile(cwd2, relPath, content) {
|
|
256
|
+
const abs = join2(cwd2, relPath);
|
|
257
|
+
if (existsSync2(abs)) return;
|
|
258
|
+
mkdirSync2(resolve(abs, ".."), { recursive: true });
|
|
259
|
+
writeFileSync2(abs, content, "utf8");
|
|
260
|
+
}
|
|
261
|
+
function appendGitignore(cwd2) {
|
|
262
|
+
const giPath = join2(cwd2, ".gitignore");
|
|
263
|
+
const existing = existsSync2(giPath) ? readFileSync3(giPath, "utf8") : "";
|
|
264
|
+
const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
|
|
265
|
+
if (toAdd.trim()) {
|
|
266
|
+
writeFileSync2(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function slugify(title) {
|
|
270
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/core/materializer/claude-code.ts
|
|
274
|
+
var ClaudeCodeMaterializer = class {
|
|
275
|
+
async scaffold(config, opts) {
|
|
276
|
+
const { cwd: cwd2 } = opts;
|
|
277
|
+
const write = (relPath, content, mode) => {
|
|
278
|
+
const abs = join3(cwd2, relPath);
|
|
279
|
+
mkdirSync3(resolve2(abs, ".."), { recursive: true });
|
|
280
|
+
writeFileSync3(abs, content, { encoding: "utf8", mode });
|
|
281
|
+
};
|
|
282
|
+
write("AGENTS.md", agentsMd(config));
|
|
283
|
+
if (!existsSync3(join3(cwd2, "health.sh"))) {
|
|
284
|
+
write("health.sh", HEALTH_SH, 493);
|
|
285
|
+
}
|
|
286
|
+
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
287
|
+
write(join3(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
288
|
+
if (!existsSync3(join3(cwd2, config.storage.markdownFallback.path))) {
|
|
289
|
+
write(
|
|
290
|
+
config.storage.markdownFallback.path,
|
|
291
|
+
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
292
|
+
<!-- Run ahk status to refresh -->
|
|
293
|
+
|
|
294
|
+
# Current Session
|
|
295
|
+
|
|
296
|
+
No tasks in progress.
|
|
297
|
+
`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const projectName = config.project.name;
|
|
301
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
302
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
303
|
+
writeAgentFile(cwd2, ".claude/agents/lead.md", agentLead({ projectName }));
|
|
304
|
+
writeAgentFile(cwd2, ".claude/agents/explorer.md", agentExplorer({ projectName, allowedPaths }));
|
|
305
|
+
writeAgentFile(cwd2, ".claude/agents/builder.md", agentBuilder({ projectName, writablePaths }));
|
|
306
|
+
writeAgentFile(cwd2, ".claude/agents/reviewer.md", agentReviewer({ projectName }));
|
|
307
|
+
mergeClaudeMcpJson(join3(cwd2, ".mcp.json"), config.tools.mcp.port);
|
|
308
|
+
appendGitignore(cwd2);
|
|
309
|
+
}
|
|
310
|
+
async build(config, cwd2) {
|
|
311
|
+
const write = (relPath, content) => {
|
|
312
|
+
const abs = join3(cwd2, relPath);
|
|
313
|
+
mkdirSync3(resolve2(abs, ".."), { recursive: true });
|
|
314
|
+
writeFileSync3(abs, content, "utf8");
|
|
315
|
+
};
|
|
316
|
+
write("AGENTS.md", agentsMd(config));
|
|
317
|
+
const projectName = config.project.name;
|
|
318
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
319
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
320
|
+
writeAgentFile(cwd2, ".claude/agents/lead.md", agentLead({ projectName }));
|
|
321
|
+
writeAgentFile(cwd2, ".claude/agents/explorer.md", agentExplorer({ projectName, allowedPaths }));
|
|
322
|
+
writeAgentFile(cwd2, ".claude/agents/builder.md", agentBuilder({ projectName, writablePaths }));
|
|
323
|
+
writeAgentFile(cwd2, ".claude/agents/reviewer.md", agentReviewer({ projectName }));
|
|
324
|
+
mergeClaudeMcpJson(join3(cwd2, ".mcp.json"), config.tools.mcp.port);
|
|
325
|
+
}
|
|
326
|
+
async migrate(config, _to, _cwd) {
|
|
327
|
+
void config;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/core/materializer/opencode.ts
|
|
332
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
333
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
334
|
+
var OpenCodeMaterializer = class {
|
|
335
|
+
async scaffold(config, opts) {
|
|
336
|
+
const { cwd: cwd2 } = opts;
|
|
337
|
+
const write = (relPath, content, mode) => {
|
|
338
|
+
const abs = join4(cwd2, relPath);
|
|
339
|
+
mkdirSync4(resolve3(abs, ".."), { recursive: true });
|
|
340
|
+
writeFileSync4(abs, content, { encoding: "utf8", mode });
|
|
341
|
+
};
|
|
342
|
+
write("AGENTS.md", agentsMd(config));
|
|
343
|
+
if (!existsSync4(join4(cwd2, "health.sh"))) {
|
|
344
|
+
write("health.sh", HEALTH_SH, 493);
|
|
345
|
+
}
|
|
346
|
+
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
347
|
+
write(join4(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
348
|
+
if (!existsSync4(join4(cwd2, config.storage.markdownFallback.path))) {
|
|
349
|
+
write(
|
|
350
|
+
config.storage.markdownFallback.path,
|
|
351
|
+
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
352
|
+
<!-- Run ahk status to refresh -->
|
|
353
|
+
|
|
354
|
+
# Current Session
|
|
355
|
+
|
|
356
|
+
No tasks in progress.
|
|
357
|
+
`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const projectName = config.project.name;
|
|
361
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
362
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
363
|
+
writeAgentFile(cwd2, ".opencode/agents/lead.md", agentLead({ projectName }));
|
|
364
|
+
writeAgentFile(cwd2, ".opencode/agents/explorer.md", agentExplorer({ projectName, allowedPaths }));
|
|
365
|
+
writeAgentFile(cwd2, ".opencode/agents/builder.md", agentBuilder({ projectName, writablePaths }));
|
|
366
|
+
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", agentReviewer({ projectName }));
|
|
367
|
+
mergeOpencodeJson(join4(cwd2, "opencode.json"), config.tools.mcp.port);
|
|
368
|
+
appendGitignore(cwd2);
|
|
369
|
+
}
|
|
370
|
+
async build(config, cwd2) {
|
|
371
|
+
const write = (relPath, content) => {
|
|
372
|
+
const abs = join4(cwd2, relPath);
|
|
373
|
+
mkdirSync4(resolve3(abs, ".."), { recursive: true });
|
|
374
|
+
writeFileSync4(abs, content, "utf8");
|
|
375
|
+
};
|
|
376
|
+
write("AGENTS.md", agentsMd(config));
|
|
377
|
+
const projectName = config.project.name;
|
|
378
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
379
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
380
|
+
writeAgentFile(cwd2, ".opencode/agents/lead.md", agentLead({ projectName }));
|
|
381
|
+
writeAgentFile(cwd2, ".opencode/agents/explorer.md", agentExplorer({ projectName, allowedPaths }));
|
|
382
|
+
writeAgentFile(cwd2, ".opencode/agents/builder.md", agentBuilder({ projectName, writablePaths }));
|
|
383
|
+
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", agentReviewer({ projectName }));
|
|
384
|
+
mergeOpencodeJson(join4(cwd2, "opencode.json"), config.tools.mcp.port);
|
|
385
|
+
}
|
|
386
|
+
async migrate(config, _to, _cwd) {
|
|
387
|
+
void config;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/core/materializer/index.ts
|
|
392
|
+
function getMaterializer(provider) {
|
|
393
|
+
switch (provider) {
|
|
394
|
+
case "claude-code":
|
|
395
|
+
return new ClaudeCodeMaterializer();
|
|
396
|
+
case "opencode":
|
|
397
|
+
return new OpenCodeMaterializer();
|
|
398
|
+
default:
|
|
399
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/commands/build.ts
|
|
404
|
+
async function runBuild(cwd2, opts) {
|
|
405
|
+
await buildOnce(cwd2);
|
|
406
|
+
if (opts.watch) {
|
|
407
|
+
p.log.info(`Watching agent-harness-kit.config.ts for changes...`);
|
|
408
|
+
watch(cwd2, { recursive: false }, async (_, filename) => {
|
|
409
|
+
if (filename?.startsWith("agent-harness-kit.config")) {
|
|
410
|
+
p.log.step("Config changed \u2014 rebuilding...");
|
|
411
|
+
await buildOnce(cwd2);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
await new Promise(() => {
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function buildOnce(cwd2) {
|
|
419
|
+
const spinner5 = p.spinner();
|
|
420
|
+
spinner5.start("Loading config...");
|
|
421
|
+
try {
|
|
422
|
+
const config = await loadConfig(cwd2);
|
|
423
|
+
spinner5.message("Rebuilding files...");
|
|
424
|
+
const materializer = getMaterializer(config.provider);
|
|
425
|
+
await materializer.build(config, cwd2);
|
|
426
|
+
spinner5.stop(pc.green("Build complete"));
|
|
427
|
+
p.log.success("AGENTS.md");
|
|
428
|
+
p.log.success(`Agent definitions (${config.provider})`);
|
|
429
|
+
p.log.success("MCP config");
|
|
430
|
+
} catch (err) {
|
|
431
|
+
spinner5.stop(pc.red("Build failed"));
|
|
432
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/commands/dashboard.ts
|
|
438
|
+
import { dirname as dirname4, join as join7, resolve as resolve5 } from "path";
|
|
439
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
440
|
+
import pc2 from "picocolors";
|
|
441
|
+
|
|
442
|
+
// src/core/dashboard-server.ts
|
|
443
|
+
import { watch as watch2 } from "fs";
|
|
444
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
445
|
+
import { extname, join as join5 } from "path";
|
|
446
|
+
import { serve } from "@hono/node-server";
|
|
447
|
+
import { Hono } from "hono";
|
|
448
|
+
import { WebSocketServer } from "ws";
|
|
449
|
+
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
450
|
+
var MIME = {
|
|
451
|
+
".html": "text/html; charset=utf-8",
|
|
452
|
+
"": "application/javascript; charset=utf-8",
|
|
453
|
+
".css": "text/css; charset=utf-8",
|
|
454
|
+
".json": "application/json; charset=utf-8",
|
|
455
|
+
".svg": "image/svg+xml",
|
|
456
|
+
".png": "image/png",
|
|
457
|
+
".ico": "image/x-icon",
|
|
458
|
+
".woff": "font/woff",
|
|
459
|
+
".woff2": "font/woff2",
|
|
460
|
+
".ttf": "font/ttf"
|
|
461
|
+
};
|
|
462
|
+
function fileResponse(filePath) {
|
|
463
|
+
const content = readFileSync4(filePath);
|
|
464
|
+
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
465
|
+
return new Response(content, {
|
|
466
|
+
headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function startDashboardServer(db, dbPath, staticPath, port) {
|
|
470
|
+
const app = new Hono();
|
|
471
|
+
app.use("/api/*", async (c, next) => {
|
|
472
|
+
await next();
|
|
473
|
+
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
474
|
+
});
|
|
475
|
+
app.get("/api/stats", (c) => {
|
|
476
|
+
const summary = db.getStatusSummary();
|
|
477
|
+
const byStatus = { pending: 0, in_progress: 0, done: 0, blocked: 0 };
|
|
478
|
+
for (const { status, total } of summary) byStatus[status] = total;
|
|
479
|
+
const [{ total: totalActions }] = db.queryRaw(`SELECT COUNT(*) as total FROM actions`);
|
|
480
|
+
const [{ total: totalFiles }] = db.queryRaw(`SELECT COUNT(*) as total FROM action_files`);
|
|
481
|
+
const [{ total: uniqueTools }] = db.queryRaw(`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`);
|
|
482
|
+
const [{ total: activeAgents }] = db.queryRaw(
|
|
483
|
+
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
484
|
+
);
|
|
485
|
+
return c.json({ byStatus, totalActions, totalFiles, uniqueTools, activeAgents });
|
|
486
|
+
});
|
|
487
|
+
app.get("/api/meta", (c) => {
|
|
488
|
+
return c.json({ ok: true });
|
|
489
|
+
});
|
|
490
|
+
app.get("/api/tasks", (c) => {
|
|
491
|
+
const rows = db.queryRaw(`
|
|
492
|
+
SELECT t.*,
|
|
493
|
+
COUNT(ta.id) as acceptance_total,
|
|
494
|
+
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
495
|
+
FROM tasks t
|
|
496
|
+
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
497
|
+
GROUP BY t.id
|
|
498
|
+
ORDER BY t.id
|
|
499
|
+
`);
|
|
500
|
+
return c.json(rows);
|
|
501
|
+
});
|
|
502
|
+
app.get("/api/tasks/:id", (c) => {
|
|
503
|
+
const id = parseInt(c.req.param("id"));
|
|
504
|
+
const task2 = db.getTaskById(id);
|
|
505
|
+
if (!task2) return c.json({ error: "Not found" }, 404);
|
|
506
|
+
const acceptance = db.getTaskAcceptance(id);
|
|
507
|
+
const actions = db.getActionsForTask(id).map((action) => ({
|
|
508
|
+
...action,
|
|
509
|
+
sections: db.getActionSections(action.id),
|
|
510
|
+
files: db.queryRaw(`SELECT * FROM action_files WHERE action_id = ?`, action.id),
|
|
511
|
+
tools: db.queryRaw(`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`, action.id)
|
|
512
|
+
}));
|
|
513
|
+
return c.json({ ...task2, acceptance, actions });
|
|
514
|
+
});
|
|
515
|
+
app.get("/api/tools/top", (c) => {
|
|
516
|
+
const limit = parseInt(c.req.query("limit") ?? "20");
|
|
517
|
+
return c.json(db.getTopTools(limit));
|
|
518
|
+
});
|
|
519
|
+
app.get("/api/tools/recent", (c) => {
|
|
520
|
+
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
521
|
+
const rows = db.queryRaw(`
|
|
522
|
+
SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
523
|
+
FROM action_tools at
|
|
524
|
+
JOIN actions a ON at.action_id = a.id
|
|
525
|
+
JOIN tasks t ON a.task_id = t.id
|
|
526
|
+
ORDER BY at.called_at DESC
|
|
527
|
+
LIMIT ?
|
|
528
|
+
`, limit);
|
|
529
|
+
return c.json(rows);
|
|
530
|
+
});
|
|
531
|
+
app.get("/api/files/top", (c) => {
|
|
532
|
+
const limit = parseInt(c.req.query("limit") ?? "20");
|
|
533
|
+
const rows = db.queryRaw(`
|
|
534
|
+
SELECT
|
|
535
|
+
file_path,
|
|
536
|
+
COUNT(*) as total,
|
|
537
|
+
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
538
|
+
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
539
|
+
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
540
|
+
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
541
|
+
FROM action_files
|
|
542
|
+
GROUP BY file_path
|
|
543
|
+
ORDER BY total DESC
|
|
544
|
+
LIMIT ?
|
|
545
|
+
`, limit);
|
|
546
|
+
return c.json(rows);
|
|
547
|
+
});
|
|
548
|
+
app.get("/api/files/recent", (c) => {
|
|
549
|
+
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
550
|
+
const rows = db.queryRaw(`
|
|
551
|
+
SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
552
|
+
a.agent, a.created_at as called_at
|
|
553
|
+
FROM action_files af
|
|
554
|
+
JOIN actions a ON af.action_id = a.id
|
|
555
|
+
JOIN tasks t ON a.task_id = t.id
|
|
556
|
+
ORDER BY a.created_at DESC
|
|
557
|
+
LIMIT ?
|
|
558
|
+
`, limit);
|
|
559
|
+
return c.json(rows);
|
|
560
|
+
});
|
|
561
|
+
app.get("/api/agents/stats", (c) => {
|
|
562
|
+
const rows = db.queryRaw(`
|
|
563
|
+
SELECT
|
|
564
|
+
a.agent,
|
|
565
|
+
COUNT(*) as actions_total,
|
|
566
|
+
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
567
|
+
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
568
|
+
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
569
|
+
COUNT(DISTINCT af.file_path) as files_touched
|
|
570
|
+
FROM actions a
|
|
571
|
+
LEFT JOIN action_files af ON af.action_id = a.id
|
|
572
|
+
GROUP BY a.agent
|
|
573
|
+
ORDER BY actions_total DESC
|
|
574
|
+
`);
|
|
575
|
+
const sorted = rows.sort((a, b) => {
|
|
576
|
+
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
577
|
+
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
578
|
+
if (ai === -1 && bi === -1) return 0;
|
|
579
|
+
if (ai === -1) return 1;
|
|
580
|
+
if (bi === -1) return -1;
|
|
581
|
+
return ai - bi;
|
|
582
|
+
});
|
|
583
|
+
return c.json(sorted);
|
|
584
|
+
});
|
|
585
|
+
app.get("/api/timeline", (c) => {
|
|
586
|
+
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
587
|
+
const rows = db.queryRaw(`
|
|
588
|
+
SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
589
|
+
FROM actions a
|
|
590
|
+
JOIN tasks t ON a.task_id = t.id
|
|
591
|
+
ORDER BY a.created_at DESC
|
|
592
|
+
LIMIT ?
|
|
593
|
+
`, limit);
|
|
594
|
+
return c.json(rows);
|
|
595
|
+
});
|
|
596
|
+
app.get("/*", (c) => {
|
|
597
|
+
const urlPath = c.req.path;
|
|
598
|
+
if (urlPath !== "/") {
|
|
599
|
+
const candidate = join5(staticPath, urlPath);
|
|
600
|
+
if (existsSync5(candidate)) {
|
|
601
|
+
try {
|
|
602
|
+
return fileResponse(candidate);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return fileResponse(join5(staticPath, "index.html"));
|
|
608
|
+
});
|
|
609
|
+
const httpServer = serve({ fetch: app.fetch, port });
|
|
610
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
611
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
612
|
+
if (req.url === "/ws") {
|
|
613
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
614
|
+
wss.emit("connection", ws, req);
|
|
615
|
+
});
|
|
616
|
+
} else {
|
|
617
|
+
socket.destroy();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
let debounce;
|
|
621
|
+
const broadcast = () => {
|
|
622
|
+
clearTimeout(debounce);
|
|
623
|
+
debounce = setTimeout(() => {
|
|
624
|
+
for (const client of wss.clients) {
|
|
625
|
+
if (client.readyState === 1) {
|
|
626
|
+
client.send(JSON.stringify({ type: "update" }));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}, 150);
|
|
630
|
+
};
|
|
631
|
+
const walPath = `${dbPath}-wal`;
|
|
632
|
+
const watchTarget = existsSync5(walPath) ? walPath : dbPath;
|
|
633
|
+
const watcher = watch2(watchTarget, broadcast);
|
|
634
|
+
return {
|
|
635
|
+
url: `http://localhost:${port}`,
|
|
636
|
+
close: () => {
|
|
637
|
+
clearTimeout(debounce);
|
|
638
|
+
watcher.close();
|
|
639
|
+
wss.close();
|
|
640
|
+
httpServer.close();
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/core/db.ts
|
|
646
|
+
import { randomUUID } from "crypto";
|
|
647
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
648
|
+
import { dirname as dirname3, join as join6, resolve as resolve4 } from "path";
|
|
649
|
+
|
|
650
|
+
// src/core/sqlite-adapter.ts
|
|
651
|
+
import { createRequire } from "module";
|
|
652
|
+
var _require = createRequire(import.meta.url);
|
|
653
|
+
var isBun = "bun" in process.versions;
|
|
654
|
+
function openSQLite(path) {
|
|
655
|
+
if (isBun) {
|
|
656
|
+
const { Database } = _require("bun:sqlite");
|
|
657
|
+
return new Database(path);
|
|
658
|
+
}
|
|
659
|
+
const { DatabaseSync } = _require("node:sqlite");
|
|
660
|
+
return new DatabaseSync(path);
|
|
661
|
+
}
|
|
662
|
+
function lastInsertId(db) {
|
|
663
|
+
const row = db.prepare("SELECT last_insert_rowid() AS id").get();
|
|
664
|
+
return row.id;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/core/db.ts
|
|
668
|
+
var SCHEMA = `
|
|
669
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
670
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
671
|
+
slug TEXT NOT NULL UNIQUE,
|
|
672
|
+
title TEXT NOT NULL,
|
|
673
|
+
description TEXT,
|
|
674
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
675
|
+
CHECK(status IN ('pending','in_progress','done','blocked')),
|
|
676
|
+
assigned_to TEXT,
|
|
677
|
+
created_at TEXT NOT NULL,
|
|
678
|
+
started_at TEXT,
|
|
679
|
+
completed_at TEXT
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
CREATE TABLE IF NOT EXISTS task_acceptance (
|
|
683
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
684
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
685
|
+
criterion TEXT NOT NULL,
|
|
686
|
+
met INTEGER NOT NULL DEFAULT 0
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
CREATE TABLE IF NOT EXISTS actions (
|
|
690
|
+
id TEXT PRIMARY KEY,
|
|
691
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
692
|
+
agent TEXT NOT NULL
|
|
693
|
+
CHECK(agent IN ('lead','explorer','builder','reviewer') OR agent LIKE 'custom:%'),
|
|
694
|
+
status TEXT NOT NULL DEFAULT 'in_progress'
|
|
695
|
+
CHECK(status IN ('in_progress','completed','blocked')),
|
|
696
|
+
created_at TEXT NOT NULL,
|
|
697
|
+
completed_at TEXT,
|
|
698
|
+
summary TEXT
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
CREATE TABLE IF NOT EXISTS action_sections (
|
|
702
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
703
|
+
action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
|
|
704
|
+
section_type TEXT NOT NULL,
|
|
705
|
+
content TEXT NOT NULL,
|
|
706
|
+
created_at TEXT NOT NULL
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
CREATE TABLE IF NOT EXISTS action_files (
|
|
710
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
711
|
+
action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
|
|
712
|
+
file_path TEXT NOT NULL,
|
|
713
|
+
operation TEXT NOT NULL
|
|
714
|
+
CHECK(operation IN ('read','created','modified','deleted')),
|
|
715
|
+
notes TEXT
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
CREATE TABLE IF NOT EXISTS action_tools (
|
|
719
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
720
|
+
action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
|
|
721
|
+
tool_name TEXT NOT NULL,
|
|
722
|
+
args_json TEXT,
|
|
723
|
+
result_summary TEXT,
|
|
724
|
+
called_at TEXT NOT NULL
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
728
|
+
CREATE INDEX IF NOT EXISTS idx_actions_task_id ON actions(task_id);
|
|
729
|
+
CREATE INDEX IF NOT EXISTS idx_actions_agent ON actions(agent);
|
|
730
|
+
CREATE INDEX IF NOT EXISTS idx_actions_status ON actions(status);
|
|
731
|
+
CREATE INDEX IF NOT EXISTS idx_action_files_path ON action_files(file_path);
|
|
732
|
+
CREATE INDEX IF NOT EXISTS idx_action_tools_name ON action_tools(tool_name);
|
|
733
|
+
`;
|
|
734
|
+
var HarnessDB = class {
|
|
735
|
+
db;
|
|
736
|
+
config;
|
|
737
|
+
constructor(dbPath, config) {
|
|
738
|
+
this.config = config;
|
|
739
|
+
const abs = resolve4(dbPath);
|
|
740
|
+
mkdirSync5(dirname3(abs), { recursive: true });
|
|
741
|
+
this.db = openSQLite(abs);
|
|
742
|
+
this.db.exec(`PRAGMA journal_mode = WAL`);
|
|
743
|
+
this.db.exec(`PRAGMA foreign_keys = ON`);
|
|
744
|
+
this.db.exec(SCHEMA);
|
|
745
|
+
}
|
|
746
|
+
// ─── Tasks ────────────────────────────────────────────────────────────────
|
|
747
|
+
addTask(params) {
|
|
748
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
749
|
+
this.db.prepare(
|
|
750
|
+
`INSERT INTO tasks (slug, title, description, status, created_at)
|
|
751
|
+
VALUES (@slug, @title, @description, 'pending', @created_at)`
|
|
752
|
+
).run({
|
|
753
|
+
slug: params.slug,
|
|
754
|
+
title: params.title,
|
|
755
|
+
description: params.description ?? null,
|
|
756
|
+
created_at: now
|
|
757
|
+
});
|
|
758
|
+
const taskId = lastInsertId(this.db);
|
|
759
|
+
if (params.acceptance?.length) {
|
|
760
|
+
const accStmt = this.db.prepare(
|
|
761
|
+
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`
|
|
762
|
+
);
|
|
763
|
+
for (const criterion of params.acceptance) {
|
|
764
|
+
accStmt.run(taskId, criterion);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
this.regenerateCurrentMd();
|
|
768
|
+
return this.getTaskById(taskId);
|
|
769
|
+
}
|
|
770
|
+
getTasks(status) {
|
|
771
|
+
if (status) {
|
|
772
|
+
return this.db.prepare(`SELECT * FROM tasks WHERE status = ? ORDER BY id`).all(status);
|
|
773
|
+
}
|
|
774
|
+
return this.db.prepare(`SELECT * FROM tasks ORDER BY id`).all();
|
|
775
|
+
}
|
|
776
|
+
getTaskById(id) {
|
|
777
|
+
return this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(id) ?? null;
|
|
778
|
+
}
|
|
779
|
+
getTaskBySlug(slug) {
|
|
780
|
+
return this.db.prepare(`SELECT * FROM tasks WHERE slug = ?`).get(slug) ?? null;
|
|
781
|
+
}
|
|
782
|
+
getTaskAcceptance(taskId) {
|
|
783
|
+
return this.db.prepare(`SELECT * FROM task_acceptance WHERE task_id = ?`).all(taskId);
|
|
784
|
+
}
|
|
785
|
+
updateTaskStatus(idOrSlug, status) {
|
|
786
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
787
|
+
const task2 = typeof idOrSlug === "number" ? this.getTaskById(idOrSlug) : this.getTaskBySlug(idOrSlug);
|
|
788
|
+
if (!task2) throw new Error(`Task not found: ${idOrSlug}`);
|
|
789
|
+
if (status === "in_progress" && !task2.started_at) {
|
|
790
|
+
this.db.prepare(
|
|
791
|
+
`UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`
|
|
792
|
+
).run(status, now, task2.id);
|
|
793
|
+
} else if (status === "done") {
|
|
794
|
+
this.db.prepare(
|
|
795
|
+
`UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`
|
|
796
|
+
).run(status, now, task2.id);
|
|
797
|
+
} else {
|
|
798
|
+
this.db.prepare(`UPDATE tasks SET status = ? WHERE id = ?`).run(status, task2.id);
|
|
799
|
+
}
|
|
800
|
+
this.regenerateCurrentMd();
|
|
801
|
+
return this.getTaskById(task2.id);
|
|
802
|
+
}
|
|
803
|
+
claimTask(id, agent) {
|
|
804
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
805
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
806
|
+
try {
|
|
807
|
+
this.db.prepare(
|
|
808
|
+
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?
|
|
809
|
+
WHERE id = ? AND status = 'pending'`
|
|
810
|
+
).run(agent, now, id);
|
|
811
|
+
this.db.exec("COMMIT");
|
|
812
|
+
const task2 = this.getTaskById(id);
|
|
813
|
+
if (!task2 || task2.status !== "in_progress" || task2.assigned_to !== agent) return null;
|
|
814
|
+
this.regenerateCurrentMd();
|
|
815
|
+
return task2;
|
|
816
|
+
} catch (err) {
|
|
817
|
+
this.db.exec("ROLLBACK");
|
|
818
|
+
throw err;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
822
|
+
startAction(taskId, agent) {
|
|
823
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
824
|
+
const id = randomUUID();
|
|
825
|
+
this.db.prepare(
|
|
826
|
+
`INSERT INTO actions (id, task_id, agent, status, created_at)
|
|
827
|
+
VALUES (?, ?, ?, 'in_progress', ?)`
|
|
828
|
+
).run(id, taskId, agent, now);
|
|
829
|
+
this.regenerateCurrentMd();
|
|
830
|
+
return this.getAction(id);
|
|
831
|
+
}
|
|
832
|
+
writeSection(actionId, sectionType, content) {
|
|
833
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
834
|
+
this.db.prepare(
|
|
835
|
+
`INSERT INTO action_sections (action_id, section_type, content, created_at)
|
|
836
|
+
VALUES (?, ?, ?, ?)`
|
|
837
|
+
).run(actionId, sectionType, content, now);
|
|
838
|
+
this.regenerateCurrentMd();
|
|
839
|
+
}
|
|
840
|
+
completeAction(actionId, summary) {
|
|
841
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
842
|
+
this.db.prepare(
|
|
843
|
+
`UPDATE actions SET status = 'completed', completed_at = ?, summary = ?
|
|
844
|
+
WHERE id = ?`
|
|
845
|
+
).run(now, summary, actionId);
|
|
846
|
+
this.regenerateCurrentMd();
|
|
847
|
+
return this.getAction(actionId);
|
|
848
|
+
}
|
|
849
|
+
getAction(actionId) {
|
|
850
|
+
return this.db.prepare(`SELECT * FROM actions WHERE id = ?`).get(actionId) ?? null;
|
|
851
|
+
}
|
|
852
|
+
getActionsForTask(taskId) {
|
|
853
|
+
return this.db.prepare(`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`).all(taskId);
|
|
854
|
+
}
|
|
855
|
+
getActionSections(actionId) {
|
|
856
|
+
return this.db.prepare(
|
|
857
|
+
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`
|
|
858
|
+
).all(actionId);
|
|
859
|
+
}
|
|
860
|
+
recordFile(actionId, filePath, operation, notes) {
|
|
861
|
+
this.db.prepare(
|
|
862
|
+
`INSERT INTO action_files (action_id, file_path, operation, notes)
|
|
863
|
+
VALUES (?, ?, ?, ?)`
|
|
864
|
+
).run(actionId, filePath, operation, notes ?? null);
|
|
865
|
+
}
|
|
866
|
+
recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
867
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
868
|
+
this.db.prepare(
|
|
869
|
+
`INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at)
|
|
870
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
871
|
+
).run(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
|
|
872
|
+
}
|
|
873
|
+
getFilesForTask(taskId) {
|
|
874
|
+
return this.db.prepare(
|
|
875
|
+
`SELECT af.*, a.agent
|
|
876
|
+
FROM action_files af
|
|
877
|
+
JOIN actions a ON af.action_id = a.id
|
|
878
|
+
WHERE a.task_id = ?
|
|
879
|
+
ORDER BY a.agent, af.operation`
|
|
880
|
+
).all(taskId);
|
|
881
|
+
}
|
|
882
|
+
getTopTools(limit = 10) {
|
|
883
|
+
return this.db.prepare(
|
|
884
|
+
`SELECT tool_name, COUNT(*) as uses
|
|
885
|
+
FROM action_tools
|
|
886
|
+
GROUP BY tool_name
|
|
887
|
+
ORDER BY uses DESC
|
|
888
|
+
LIMIT ?`
|
|
889
|
+
).all(limit);
|
|
890
|
+
}
|
|
891
|
+
getStatusSummary() {
|
|
892
|
+
return this.db.prepare(`SELECT status, COUNT(*) as total FROM tasks GROUP BY status`).all();
|
|
893
|
+
}
|
|
894
|
+
// ─── current.md fallback ──────────────────────────────────────────────────
|
|
895
|
+
regenerateCurrentMd() {
|
|
896
|
+
if (!this.config.storage.markdownFallback.enabled) return;
|
|
897
|
+
const mdPath = resolve4(this.config.storage.markdownFallback.path);
|
|
898
|
+
mkdirSync5(dirname3(mdPath), { recursive: true });
|
|
899
|
+
const inProgress = this.getTasks("in_progress");
|
|
900
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
901
|
+
let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
902
|
+
`;
|
|
903
|
+
md += `<!-- Last updated: ${now} -->
|
|
904
|
+
|
|
905
|
+
`;
|
|
906
|
+
md += `# Current Session
|
|
907
|
+
|
|
908
|
+
`;
|
|
909
|
+
if (inProgress.length === 0) {
|
|
910
|
+
md += `## No tasks in progress
|
|
911
|
+
|
|
912
|
+
`;
|
|
913
|
+
const pending = this.getTasks("pending");
|
|
914
|
+
if (pending.length > 0) {
|
|
915
|
+
md += `### Next pending tasks
|
|
916
|
+
`;
|
|
917
|
+
for (const t of pending.slice(0, 5)) {
|
|
918
|
+
md += `- **#${t.id}** ${t.title} (\`${t.slug}\`)
|
|
919
|
+
`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
for (const task2 of inProgress) {
|
|
924
|
+
md += `## Active Task
|
|
925
|
+
`;
|
|
926
|
+
md += `- **ID:** ${task2.id}
|
|
927
|
+
`;
|
|
928
|
+
md += `- **Slug:** ${task2.slug}
|
|
929
|
+
`;
|
|
930
|
+
md += `- **Status:** ${task2.status}
|
|
931
|
+
`;
|
|
932
|
+
md += `- **Started:** ${task2.started_at ?? "unknown"}
|
|
933
|
+
|
|
934
|
+
`;
|
|
935
|
+
const actions = this.getActionsForTask(task2.id);
|
|
936
|
+
if (actions.length > 0) {
|
|
937
|
+
md += `## Actions this session
|
|
938
|
+
`;
|
|
939
|
+
md += `| Agent | Status | Summary | Started |
|
|
940
|
+
`;
|
|
941
|
+
md += `|----------|-------------|----------------------------------|-------------|
|
|
942
|
+
`;
|
|
943
|
+
for (const a of actions) {
|
|
944
|
+
const started = a.created_at.slice(11, 16);
|
|
945
|
+
const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
|
|
946
|
+
md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
|
|
947
|
+
`;
|
|
948
|
+
}
|
|
949
|
+
md += `
|
|
950
|
+
`;
|
|
951
|
+
}
|
|
952
|
+
const acceptance = this.getTaskAcceptance(task2.id);
|
|
953
|
+
if (acceptance.length > 0) {
|
|
954
|
+
md += `## Acceptance Criteria
|
|
955
|
+
`;
|
|
956
|
+
for (const a of acceptance) {
|
|
957
|
+
md += `- [${a.met ? "x" : " "}] ${a.criterion}
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
md += `
|
|
961
|
+
`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
writeFileSync5(mdPath, md, "utf8");
|
|
966
|
+
}
|
|
967
|
+
// ─── Raw query (dashboard / analytics) ───────────────────────────────────
|
|
968
|
+
queryRaw(sql, ...params) {
|
|
969
|
+
return this.db.prepare(sql).all(...params);
|
|
970
|
+
}
|
|
971
|
+
// ─── Export helpers ───────────────────────────────────────────────────────
|
|
972
|
+
exportJson() {
|
|
973
|
+
return {
|
|
974
|
+
tasks: this.getTasks(),
|
|
975
|
+
actions: this.db.prepare(`SELECT * FROM actions ORDER BY created_at`).all(),
|
|
976
|
+
sections: this.db.prepare(`SELECT * FROM action_sections ORDER BY created_at`).all()
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
close() {
|
|
980
|
+
this.db.close();
|
|
981
|
+
}
|
|
982
|
+
// ─── feature_list.json sync ───────────────────────────────────────────────
|
|
983
|
+
syncFromFeatureList(tasks) {
|
|
984
|
+
let added = 0;
|
|
985
|
+
let skipped = 0;
|
|
986
|
+
for (const t of tasks) {
|
|
987
|
+
if (this.getTaskBySlug(t.slug)) {
|
|
988
|
+
skipped++;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
this.addTask(t);
|
|
992
|
+
added++;
|
|
993
|
+
}
|
|
994
|
+
return { added, skipped };
|
|
995
|
+
}
|
|
996
|
+
writeFeatureList(cwd2) {
|
|
997
|
+
const tasks = this.getTasks();
|
|
998
|
+
const list = tasks.map((t) => ({
|
|
999
|
+
slug: t.slug,
|
|
1000
|
+
title: t.title,
|
|
1001
|
+
description: t.description ?? void 0,
|
|
1002
|
+
acceptance: this.getTaskAcceptance(t.id).map((a) => a.criterion),
|
|
1003
|
+
status: t.status
|
|
1004
|
+
}));
|
|
1005
|
+
const path = join6(resolve4(cwd2), this.config.storage.dir, "feature_list.json");
|
|
1006
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
1007
|
+
writeFileSync5(path, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
function openDB(config, cwd2) {
|
|
1011
|
+
const dbPath = join6(resolve4(cwd2), config.storage.dbPath);
|
|
1012
|
+
return new HarnessDB(dbPath, config);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/commands/dashboard.ts
|
|
1016
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
1017
|
+
async function runDashboard(cwd2, opts) {
|
|
1018
|
+
const config = await loadConfig(cwd2);
|
|
1019
|
+
const db = openDB(config, cwd2);
|
|
1020
|
+
const dbPath = resolve5(cwd2, config.storage.dbPath);
|
|
1021
|
+
const staticPath = join7(__dirname2, "..", "dashboard-dist");
|
|
1022
|
+
const { url } = startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1023
|
+
console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
|
|
1024
|
+
console.log(pc2.dim(` WebSocket live updates enabled`));
|
|
1025
|
+
console.log(pc2.dim(` Press Ctrl+C to stop`));
|
|
1026
|
+
if (opts.open) {
|
|
1027
|
+
const { default: open } = await import("open");
|
|
1028
|
+
await open(url);
|
|
1029
|
+
}
|
|
1030
|
+
process.on("SIGINT", () => {
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
});
|
|
1033
|
+
await new Promise(() => {
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/commands/export.ts
|
|
1038
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
1039
|
+
import pc3 from "picocolors";
|
|
1040
|
+
async function runExport(cwd2, opts) {
|
|
1041
|
+
if (!opts.sql && !opts.json) {
|
|
1042
|
+
console.error(pc3.red("Specify --sql or --json"));
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
const config = await loadConfig(cwd2);
|
|
1046
|
+
const db = openDB(config, cwd2);
|
|
1047
|
+
try {
|
|
1048
|
+
if (opts.json) {
|
|
1049
|
+
const data = db.exportJson();
|
|
1050
|
+
const out = JSON.stringify(data, null, 2) + "\n";
|
|
1051
|
+
if (opts.output) {
|
|
1052
|
+
writeFileSync6(opts.output, out, "utf8");
|
|
1053
|
+
console.log(pc3.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
|
|
1054
|
+
} else {
|
|
1055
|
+
process.stdout.write(out);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (opts.sql) {
|
|
1059
|
+
console.error(pc3.dim("SQL dump requires direct SQLite access \u2014 use: sqlite3 .harness/harness.db .dump"));
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
} finally {
|
|
1063
|
+
db.close();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/commands/health.ts
|
|
1068
|
+
import { spawnSync } from "child_process";
|
|
1069
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1070
|
+
import { join as join8, resolve as resolve6 } from "path";
|
|
1071
|
+
import pc4 from "picocolors";
|
|
1072
|
+
function checkLine(label, ok2, message, indent = 0) {
|
|
1073
|
+
const prefix = label ? pc4.cyan(`[${label}] `) : " ".repeat(indent);
|
|
1074
|
+
const icon = ok2 ? pc4.green("\u2713") : pc4.red("\u2717");
|
|
1075
|
+
console.log(prefix + icon + " " + (ok2 ? pc4.green(message) : pc4.red(message)));
|
|
1076
|
+
}
|
|
1077
|
+
async function runHealth(cwd2) {
|
|
1078
|
+
let config;
|
|
1079
|
+
try {
|
|
1080
|
+
config = await loadConfig(cwd2);
|
|
1081
|
+
} catch {
|
|
1082
|
+
console.error(pc4.red("\u2717 No config found. Run: ahk init"));
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
let allOk = true;
|
|
1086
|
+
const dbPath = resolve6(cwd2, config.storage.dbPath);
|
|
1087
|
+
const dbOk = existsSync6(dbPath);
|
|
1088
|
+
checkLine("checking DB", dbOk, `${config.storage.dbPath} reachable`);
|
|
1089
|
+
if (!dbOk) allOk = false;
|
|
1090
|
+
const agentsDir = config.provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
1091
|
+
const agentNames = ["lead", "explorer", "builder", "reviewer"];
|
|
1092
|
+
const agentsLabelWidth = "[checking agents] ".length;
|
|
1093
|
+
for (let i = 0; i < agentNames.length; i++) {
|
|
1094
|
+
const name = agentNames[i];
|
|
1095
|
+
const agentPath = join8(cwd2, agentsDir, `${name}.md`);
|
|
1096
|
+
const ok2 = existsSync6(agentPath);
|
|
1097
|
+
checkLine(i === 0 ? "checking agents" : null, ok2, `${name}.md present`, agentsLabelWidth);
|
|
1098
|
+
if (!ok2) allOk = false;
|
|
1099
|
+
}
|
|
1100
|
+
if (config.tools.mcp.enabled) {
|
|
1101
|
+
const mcpFile = config.provider === "claude-code" ? ".claude/mcp.json" : "opencode.json";
|
|
1102
|
+
const mcpPath = resolve6(cwd2, mcpFile);
|
|
1103
|
+
const mcpOk = existsSync6(mcpPath);
|
|
1104
|
+
checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
|
|
1105
|
+
if (!mcpOk) allOk = false;
|
|
1106
|
+
}
|
|
1107
|
+
if (!allOk) {
|
|
1108
|
+
console.log("");
|
|
1109
|
+
console.error(pc4.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
const scriptPath = resolve6(cwd2, config.health.scriptPath);
|
|
1113
|
+
if (!existsSync6(scriptPath)) {
|
|
1114
|
+
console.error(pc4.red(`\u2717 health.sh not found: ${scriptPath}`));
|
|
1115
|
+
console.error(" Run ahk init first.");
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
const result = spawnSync("bash", [scriptPath], {
|
|
1119
|
+
cwd: cwd2,
|
|
1120
|
+
stdio: "inherit",
|
|
1121
|
+
encoding: "utf8"
|
|
1122
|
+
});
|
|
1123
|
+
if (result.error) {
|
|
1124
|
+
console.error(pc4.red(`\u2717 Failed to run health.sh: ${result.error.message}`));
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
if (result.status === 0) {
|
|
1128
|
+
console.log(pc4.green("\u2713 Health check passed"));
|
|
1129
|
+
process.exit(0);
|
|
1130
|
+
} else {
|
|
1131
|
+
console.error(pc4.red(`\u2717 Health check failed (exit ${result.status ?? "unknown"})`));
|
|
1132
|
+
process.exit(result.status ?? 1);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/commands/init.ts
|
|
1137
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1138
|
+
import { join as join9 } from "path";
|
|
1139
|
+
import * as p2 from "@clack/prompts";
|
|
1140
|
+
import pc5 from "picocolors";
|
|
1141
|
+
|
|
1142
|
+
// src/commands/init-helpers.ts
|
|
1143
|
+
function applyConfigDefaults(params) {
|
|
1144
|
+
return {
|
|
1145
|
+
provider: params.provider,
|
|
1146
|
+
project: {
|
|
1147
|
+
name: params.name,
|
|
1148
|
+
description: params.description,
|
|
1149
|
+
docsPath: params.docsPath,
|
|
1150
|
+
agentsMd: "./AGENTS.md"
|
|
1151
|
+
},
|
|
1152
|
+
agents: {
|
|
1153
|
+
lead: { instructionsPath: null },
|
|
1154
|
+
explorer: { instructionsPath: null, allowedPaths: [params.docsPath, "./src"] },
|
|
1155
|
+
builder: { instructionsPath: null, writablePaths: ["./src", "./tests"] },
|
|
1156
|
+
reviewer: { instructionsPath: null },
|
|
1157
|
+
custom: []
|
|
1158
|
+
},
|
|
1159
|
+
storage: {
|
|
1160
|
+
dir: ".harness",
|
|
1161
|
+
dbPath: ".harness/harness.db",
|
|
1162
|
+
tasks: { adapter: params.tasksAdapter },
|
|
1163
|
+
sections: {
|
|
1164
|
+
toolsUsed: true,
|
|
1165
|
+
filesModified: true,
|
|
1166
|
+
result: true,
|
|
1167
|
+
blockers: true,
|
|
1168
|
+
nextSteps: false
|
|
1169
|
+
},
|
|
1170
|
+
markdownFallback: { enabled: true, path: ".harness/current.md" }
|
|
1171
|
+
},
|
|
1172
|
+
health: {
|
|
1173
|
+
scriptPath: "./health.sh",
|
|
1174
|
+
required: true
|
|
1175
|
+
},
|
|
1176
|
+
tools: {
|
|
1177
|
+
mcp: { enabled: true, port: 3742 },
|
|
1178
|
+
scripts: { enabled: true, outputDir: "./.harness/scripts" }
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/init.ts
|
|
1184
|
+
async function runInit(cwd2, flags) {
|
|
1185
|
+
p2.intro(pc5.bold("agent-harness-kit \u2014 harness scaffolding"));
|
|
1186
|
+
let name;
|
|
1187
|
+
if (flags.name) {
|
|
1188
|
+
name = flags.name;
|
|
1189
|
+
} else {
|
|
1190
|
+
const val = await p2.text({
|
|
1191
|
+
message: "Project name",
|
|
1192
|
+
placeholder: "my-app",
|
|
1193
|
+
validate: (v) => v.trim() ? void 0 : "Project name is required"
|
|
1194
|
+
});
|
|
1195
|
+
if (p2.isCancel(val)) {
|
|
1196
|
+
p2.cancel("Cancelled.");
|
|
1197
|
+
process.exit(0);
|
|
1198
|
+
}
|
|
1199
|
+
name = val;
|
|
1200
|
+
}
|
|
1201
|
+
const descVal = await p2.text({
|
|
1202
|
+
message: "Short description (shown to agents as context)",
|
|
1203
|
+
placeholder: "A REST API for managing notes"
|
|
1204
|
+
});
|
|
1205
|
+
if (p2.isCancel(descVal)) {
|
|
1206
|
+
p2.cancel("Cancelled.");
|
|
1207
|
+
process.exit(0);
|
|
1208
|
+
}
|
|
1209
|
+
const description = descVal.trim() || name;
|
|
1210
|
+
let provider;
|
|
1211
|
+
if (flags.provider && ["claude-code", "opencode"].includes(flags.provider)) {
|
|
1212
|
+
provider = flags.provider;
|
|
1213
|
+
} else {
|
|
1214
|
+
const val = await p2.select({
|
|
1215
|
+
message: "AI provider",
|
|
1216
|
+
options: [
|
|
1217
|
+
{ value: "claude-code", label: "Claude Code" },
|
|
1218
|
+
{ value: "opencode", label: "OpenCode" }
|
|
1219
|
+
]
|
|
1220
|
+
});
|
|
1221
|
+
if (p2.isCancel(val)) {
|
|
1222
|
+
p2.cancel("Cancelled.");
|
|
1223
|
+
process.exit(0);
|
|
1224
|
+
}
|
|
1225
|
+
provider = val;
|
|
1226
|
+
}
|
|
1227
|
+
let docsPath;
|
|
1228
|
+
if (flags.docs) {
|
|
1229
|
+
docsPath = flags.docs;
|
|
1230
|
+
} else {
|
|
1231
|
+
const val = await p2.text({
|
|
1232
|
+
message: "Docs folder path (agents will search here)",
|
|
1233
|
+
initialValue: "./docs"
|
|
1234
|
+
});
|
|
1235
|
+
if (p2.isCancel(val)) {
|
|
1236
|
+
p2.cancel("Cancelled.");
|
|
1237
|
+
process.exit(0);
|
|
1238
|
+
}
|
|
1239
|
+
docsPath = val.trim() || "./docs";
|
|
1240
|
+
}
|
|
1241
|
+
let tasksAdapter;
|
|
1242
|
+
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
1243
|
+
tasksAdapter = flags.tasks;
|
|
1244
|
+
} else {
|
|
1245
|
+
const val = await p2.select({
|
|
1246
|
+
message: "Task adapter",
|
|
1247
|
+
options: [
|
|
1248
|
+
{ value: "local", label: "Local (feature_list.json)" },
|
|
1249
|
+
{ value: "jira", label: "Jira (coming soon)" },
|
|
1250
|
+
{ value: "linear", label: "Linear (coming soon)" }
|
|
1251
|
+
]
|
|
1252
|
+
});
|
|
1253
|
+
if (p2.isCancel(val)) {
|
|
1254
|
+
p2.cancel("Cancelled.");
|
|
1255
|
+
process.exit(0);
|
|
1256
|
+
}
|
|
1257
|
+
tasksAdapter = val;
|
|
1258
|
+
}
|
|
1259
|
+
const addFirstTask = await p2.confirm({ message: "Add your first task now?", initialValue: true });
|
|
1260
|
+
if (p2.isCancel(addFirstTask)) {
|
|
1261
|
+
p2.cancel("Cancelled.");
|
|
1262
|
+
process.exit(0);
|
|
1263
|
+
}
|
|
1264
|
+
let firstTask;
|
|
1265
|
+
if (addFirstTask) {
|
|
1266
|
+
const titleVal = await p2.text({
|
|
1267
|
+
message: "Task title",
|
|
1268
|
+
validate: (v) => v.trim() ? void 0 : "Title is required"
|
|
1269
|
+
});
|
|
1270
|
+
if (p2.isCancel(titleVal)) {
|
|
1271
|
+
p2.cancel("Cancelled.");
|
|
1272
|
+
process.exit(0);
|
|
1273
|
+
}
|
|
1274
|
+
const taskTitle = titleVal.trim();
|
|
1275
|
+
const taskDescVal = await p2.text({
|
|
1276
|
+
message: "Task description",
|
|
1277
|
+
placeholder: "What and why"
|
|
1278
|
+
});
|
|
1279
|
+
if (p2.isCancel(taskDescVal)) {
|
|
1280
|
+
p2.cancel("Cancelled.");
|
|
1281
|
+
process.exit(0);
|
|
1282
|
+
}
|
|
1283
|
+
const taskDesc = taskDescVal.trim();
|
|
1284
|
+
const acceptance = [];
|
|
1285
|
+
p2.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
1286
|
+
while (true) {
|
|
1287
|
+
const criterionVal = await p2.text({
|
|
1288
|
+
message: ">",
|
|
1289
|
+
placeholder: "Criterion (or press Enter to finish)"
|
|
1290
|
+
});
|
|
1291
|
+
if (p2.isCancel(criterionVal) || !criterionVal.trim()) break;
|
|
1292
|
+
acceptance.push(criterionVal.trim());
|
|
1293
|
+
}
|
|
1294
|
+
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
1295
|
+
}
|
|
1296
|
+
const spinner5 = p2.spinner();
|
|
1297
|
+
spinner5.start("Scaffolding...");
|
|
1298
|
+
try {
|
|
1299
|
+
const config = applyConfigDefaults({ name, description, provider, docsPath, tasksAdapter });
|
|
1300
|
+
const materializer = getMaterializer(provider);
|
|
1301
|
+
const configContent = configTs({
|
|
1302
|
+
name,
|
|
1303
|
+
description,
|
|
1304
|
+
provider,
|
|
1305
|
+
docsPath,
|
|
1306
|
+
tasksAdapter,
|
|
1307
|
+
port: config.tools.mcp.port
|
|
1308
|
+
});
|
|
1309
|
+
writeFileSync7(join9(cwd2, "agent-harness-kit.config.ts"), configContent, "utf8");
|
|
1310
|
+
mkdirSync6(join9(cwd2, config.storage.dir), { recursive: true });
|
|
1311
|
+
const db = openDB(config, cwd2);
|
|
1312
|
+
await materializer.scaffold(config, { cwd: cwd2, firstTask });
|
|
1313
|
+
if (firstTask) {
|
|
1314
|
+
const slug = slugify(firstTask.title);
|
|
1315
|
+
db.addTask({
|
|
1316
|
+
slug,
|
|
1317
|
+
title: firstTask.title,
|
|
1318
|
+
description: firstTask.description,
|
|
1319
|
+
acceptance: firstTask.acceptance
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
db.close();
|
|
1323
|
+
spinner5.stop("");
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
spinner5.stop("Failed");
|
|
1326
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
|
|
1330
|
+
const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode/opencode.json";
|
|
1331
|
+
console.log("");
|
|
1332
|
+
console.log(pc5.green("\u2713 agent-harness-kit.config.ts"));
|
|
1333
|
+
console.log(pc5.green("\u2713 AGENTS.md"));
|
|
1334
|
+
console.log(pc5.green("\u2713 health.sh"));
|
|
1335
|
+
console.log(pc5.green("\u2713 .harness/harness.db"));
|
|
1336
|
+
console.log(pc5.green("\u2713 .harness/current.md"));
|
|
1337
|
+
console.log(pc5.green(`\u2713 ${agentsDir}lead.md`));
|
|
1338
|
+
console.log(pc5.green(`\u2713 ${agentsDir}explorer.md`));
|
|
1339
|
+
console.log(pc5.green(`\u2713 ${agentsDir}builder.md`));
|
|
1340
|
+
console.log(pc5.green(`\u2713 ${agentsDir}reviewer.md`));
|
|
1341
|
+
console.log(pc5.green(`\u2713 ${mcpFile}`));
|
|
1342
|
+
console.log(pc5.green("\u2713 .gitignore entries added"));
|
|
1343
|
+
console.log("");
|
|
1344
|
+
console.log(pc5.cyan("\u2192") + ` Edit ${pc5.cyan("health.sh")} with your project checks`);
|
|
1345
|
+
console.log(pc5.cyan("\u2192") + ` ${pc5.cyan("ahk task add")} to queue work for agents`);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/commands/migrate.ts
|
|
1349
|
+
import * as p3 from "@clack/prompts";
|
|
1350
|
+
import pc6 from "picocolors";
|
|
1351
|
+
async function runMigrate(cwd2, opts) {
|
|
1352
|
+
const config = await loadConfig(cwd2);
|
|
1353
|
+
let target;
|
|
1354
|
+
if (opts.to && ["claude-code", "opencode"].includes(opts.to)) {
|
|
1355
|
+
target = opts.to;
|
|
1356
|
+
} else {
|
|
1357
|
+
const val = await p3.select({
|
|
1358
|
+
message: "Migrate to provider",
|
|
1359
|
+
options: [
|
|
1360
|
+
{ value: "claude-code", label: "Claude Code" },
|
|
1361
|
+
{ value: "opencode", label: "OpenCode" }
|
|
1362
|
+
]
|
|
1363
|
+
});
|
|
1364
|
+
if (p3.isCancel(val)) {
|
|
1365
|
+
p3.cancel("Cancelled.");
|
|
1366
|
+
process.exit(0);
|
|
1367
|
+
}
|
|
1368
|
+
target = val;
|
|
1369
|
+
}
|
|
1370
|
+
if (target === config.provider) {
|
|
1371
|
+
console.log(pc6.dim(`Already on ${target} \u2014 nothing to migrate.`));
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const spinner5 = p3.spinner();
|
|
1375
|
+
spinner5.start(`Migrating from ${config.provider} to ${target}...`);
|
|
1376
|
+
try {
|
|
1377
|
+
const targetMaterializer = getMaterializer(target);
|
|
1378
|
+
await targetMaterializer.build(config, cwd2);
|
|
1379
|
+
spinner5.stop(pc6.green(`Migrated to ${target}`));
|
|
1380
|
+
p3.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
|
|
1381
|
+
p3.log.warn(`Then run: ahk build`);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
spinner5.stop(pc6.red("Migration failed"));
|
|
1384
|
+
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/core/mcp-server.ts
|
|
1390
|
+
import { readdirSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1391
|
+
import { join as join10, resolve as resolve7 } from "path";
|
|
1392
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index";
|
|
1393
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
|
|
1394
|
+
import {
|
|
1395
|
+
CallToolRequestSchema,
|
|
1396
|
+
ListToolsRequestSchema
|
|
1397
|
+
} from "@modelcontextprotocol/sdk/types";
|
|
1398
|
+
var VERSION = "0.1.0";
|
|
1399
|
+
var TOOLS = [
|
|
1400
|
+
{
|
|
1401
|
+
name: "actions.start",
|
|
1402
|
+
description: "Start a new action for a task. Returns an actionId (UUID).",
|
|
1403
|
+
inputSchema: {
|
|
1404
|
+
type: "object",
|
|
1405
|
+
properties: {
|
|
1406
|
+
taskId: { type: "number", description: "The task ID from tasks.get" },
|
|
1407
|
+
agent: {
|
|
1408
|
+
type: "string",
|
|
1409
|
+
description: "Agent name: lead | explorer | builder | reviewer | custom:<name>"
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
required: ["taskId", "agent"]
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
name: "actions.write",
|
|
1417
|
+
description: "Record a section in an action. Standard sections: result, tools_used, files_modified, blockers, next_steps.",
|
|
1418
|
+
inputSchema: {
|
|
1419
|
+
type: "object",
|
|
1420
|
+
properties: {
|
|
1421
|
+
actionId: { type: "string", description: "UUID returned by actions.start" },
|
|
1422
|
+
sectionType: {
|
|
1423
|
+
type: "string",
|
|
1424
|
+
description: "Section name: result | tools_used | files_modified | blockers | next_steps | <custom>"
|
|
1425
|
+
},
|
|
1426
|
+
content: { type: "string", description: "Content for this section" }
|
|
1427
|
+
},
|
|
1428
|
+
required: ["actionId", "sectionType", "content"]
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
name: "actions.complete",
|
|
1433
|
+
description: "Close an action with a one-line summary.",
|
|
1434
|
+
inputSchema: {
|
|
1435
|
+
type: "object",
|
|
1436
|
+
properties: {
|
|
1437
|
+
actionId: { type: "string", description: "UUID of the action to close" },
|
|
1438
|
+
summary: { type: "string", description: "One-line summary of what was done" }
|
|
1439
|
+
},
|
|
1440
|
+
required: ["actionId", "summary"]
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "actions.get",
|
|
1445
|
+
description: "Get the full action history for a task (all agents, all sections).",
|
|
1446
|
+
inputSchema: {
|
|
1447
|
+
type: "object",
|
|
1448
|
+
properties: {
|
|
1449
|
+
taskId: { type: "number", description: "Task ID" }
|
|
1450
|
+
},
|
|
1451
|
+
required: ["taskId"]
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
name: "tasks.get",
|
|
1456
|
+
description: "List tasks, optionally filtered by status.",
|
|
1457
|
+
inputSchema: {
|
|
1458
|
+
type: "object",
|
|
1459
|
+
properties: {
|
|
1460
|
+
status: {
|
|
1461
|
+
type: "string",
|
|
1462
|
+
enum: ["pending", "in_progress", "done", "blocked"],
|
|
1463
|
+
description: "Filter by status (omit for all tasks)"
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
name: "tasks.claim",
|
|
1470
|
+
description: "Atomically claim a pending task. Returns task_already_claimed if another agent got it first.",
|
|
1471
|
+
inputSchema: {
|
|
1472
|
+
type: "object",
|
|
1473
|
+
properties: {
|
|
1474
|
+
id: { type: "number", description: "Task ID to claim" },
|
|
1475
|
+
agent: { type: "string", description: "Your agent name" }
|
|
1476
|
+
},
|
|
1477
|
+
required: ["id", "agent"]
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
name: "tasks.update",
|
|
1482
|
+
description: "Change the status of a task.",
|
|
1483
|
+
inputSchema: {
|
|
1484
|
+
type: "object",
|
|
1485
|
+
properties: {
|
|
1486
|
+
id: { type: "number", description: "Task ID" },
|
|
1487
|
+
status: {
|
|
1488
|
+
type: "string",
|
|
1489
|
+
enum: ["pending", "in_progress", "done", "blocked"]
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
required: ["id", "status"]
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: "docs.search",
|
|
1497
|
+
description: "Search the project docs folder for content matching a query.",
|
|
1498
|
+
inputSchema: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
query: { type: "string", description: "Search terms" }
|
|
1502
|
+
},
|
|
1503
|
+
required: ["query"]
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
];
|
|
1507
|
+
async function startMcpServer(config, cwd2) {
|
|
1508
|
+
const db = openDB(config, cwd2);
|
|
1509
|
+
const docsPath = resolve7(cwd2, config.project.docsPath);
|
|
1510
|
+
const server = new Server(
|
|
1511
|
+
{ name: "agent-harness-kit", version: VERSION },
|
|
1512
|
+
{ capabilities: { tools: {} } }
|
|
1513
|
+
);
|
|
1514
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
1515
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1516
|
+
const { name, arguments: args } = request.params;
|
|
1517
|
+
const a = args ?? {};
|
|
1518
|
+
try {
|
|
1519
|
+
const result = await dispatch(name, a, db, docsPath);
|
|
1520
|
+
return result;
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
return ok(`Error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
const transport = new StdioServerTransport();
|
|
1526
|
+
await server.connect(transport);
|
|
1527
|
+
}
|
|
1528
|
+
async function dispatch(name, args, db, docsPath) {
|
|
1529
|
+
switch (name) {
|
|
1530
|
+
case "actions.start": {
|
|
1531
|
+
const taskId = num(args, "taskId");
|
|
1532
|
+
const agent = str(args, "agent");
|
|
1533
|
+
const action = db.startAction(taskId, agent);
|
|
1534
|
+
return ok(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
|
|
1535
|
+
}
|
|
1536
|
+
case "actions.write": {
|
|
1537
|
+
const actionId = str(args, "actionId");
|
|
1538
|
+
const sectionType = str(args, "sectionType");
|
|
1539
|
+
const content = str(args, "content");
|
|
1540
|
+
db.writeSection(actionId, sectionType, content);
|
|
1541
|
+
return ok(JSON.stringify({ actionId, sectionType, recorded: true }));
|
|
1542
|
+
}
|
|
1543
|
+
case "actions.complete": {
|
|
1544
|
+
const actionId = str(args, "actionId");
|
|
1545
|
+
const summary = str(args, "summary");
|
|
1546
|
+
const action = db.completeAction(actionId, summary);
|
|
1547
|
+
return ok(JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at }));
|
|
1548
|
+
}
|
|
1549
|
+
case "actions.get": {
|
|
1550
|
+
const taskId = num(args, "taskId");
|
|
1551
|
+
const actions = db.getActionsForTask(taskId);
|
|
1552
|
+
const full = actions.map((a) => ({
|
|
1553
|
+
...a,
|
|
1554
|
+
sections: db.getActionSections(a.id)
|
|
1555
|
+
}));
|
|
1556
|
+
return ok(JSON.stringify(full, null, 2));
|
|
1557
|
+
}
|
|
1558
|
+
case "tasks.get": {
|
|
1559
|
+
const status = args["status"];
|
|
1560
|
+
const tasks = status ? db.getTasks(status) : db.getTasks();
|
|
1561
|
+
return ok(JSON.stringify(tasks, null, 2));
|
|
1562
|
+
}
|
|
1563
|
+
case "tasks.claim": {
|
|
1564
|
+
const id = num(args, "id");
|
|
1565
|
+
const agent = str(args, "agent");
|
|
1566
|
+
const task2 = db.claimTask(id, agent);
|
|
1567
|
+
if (!task2) {
|
|
1568
|
+
return ok(JSON.stringify({ error: "task_already_claimed", taskId: id }));
|
|
1569
|
+
}
|
|
1570
|
+
return ok(JSON.stringify(task2));
|
|
1571
|
+
}
|
|
1572
|
+
case "tasks.update": {
|
|
1573
|
+
const id = num(args, "id");
|
|
1574
|
+
const status = str(args, "status");
|
|
1575
|
+
const task2 = db.updateTaskStatus(id, status);
|
|
1576
|
+
return ok(JSON.stringify(task2));
|
|
1577
|
+
}
|
|
1578
|
+
case "docs.search": {
|
|
1579
|
+
const query = str(args, "query");
|
|
1580
|
+
const results = searchDocs(docsPath, query);
|
|
1581
|
+
return ok(JSON.stringify(results, null, 2));
|
|
1582
|
+
}
|
|
1583
|
+
default:
|
|
1584
|
+
return ok(`Unknown tool: ${name}`, true);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function searchDocs(docsPath, query, maxResults = 10) {
|
|
1588
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1589
|
+
const results = [];
|
|
1590
|
+
try {
|
|
1591
|
+
const files = collectMarkdownFiles(docsPath);
|
|
1592
|
+
for (const file of files) {
|
|
1593
|
+
if (results.length >= maxResults) break;
|
|
1594
|
+
try {
|
|
1595
|
+
const content = readFileSync5(file, "utf8");
|
|
1596
|
+
const lines = content.split("\n");
|
|
1597
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1598
|
+
const lower = lines[i].toLowerCase();
|
|
1599
|
+
if (terms.every((t) => lower.includes(t))) {
|
|
1600
|
+
results.push({ file: file.replace(docsPath + "/", ""), line: i + 1, text: lines[i].trim() });
|
|
1601
|
+
if (results.length >= maxResults) break;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
} catch {
|
|
1608
|
+
return [{ file: "", line: 0, text: `docs path not found: ${docsPath}` }];
|
|
1609
|
+
}
|
|
1610
|
+
return results;
|
|
1611
|
+
}
|
|
1612
|
+
function collectMarkdownFiles(dir) {
|
|
1613
|
+
const files = [];
|
|
1614
|
+
try {
|
|
1615
|
+
for (const entry of readdirSync(dir)) {
|
|
1616
|
+
const full = join10(dir, entry);
|
|
1617
|
+
const stat = statSync(full);
|
|
1618
|
+
if (stat.isDirectory()) {
|
|
1619
|
+
files.push(...collectMarkdownFiles(full));
|
|
1620
|
+
} else if (entry.endsWith(".md") || entry.endsWith(".txt")) {
|
|
1621
|
+
files.push(full);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
return files;
|
|
1627
|
+
}
|
|
1628
|
+
function ok(text3, isError = false) {
|
|
1629
|
+
return { content: [{ type: "text", text: text3 }], isError };
|
|
1630
|
+
}
|
|
1631
|
+
function str(args, key) {
|
|
1632
|
+
const v = args[key];
|
|
1633
|
+
if (typeof v !== "string") throw new Error(`${key} must be a string`);
|
|
1634
|
+
return v;
|
|
1635
|
+
}
|
|
1636
|
+
function num(args, key) {
|
|
1637
|
+
const v = args[key];
|
|
1638
|
+
if (typeof v !== "number") throw new Error(`${key} must be a number`);
|
|
1639
|
+
return v;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/commands/serve.ts
|
|
1643
|
+
async function runServe(cwd2, opts) {
|
|
1644
|
+
const config = await loadConfig(cwd2);
|
|
1645
|
+
if (opts.port) {
|
|
1646
|
+
config.tools.mcp.port = opts.port;
|
|
1647
|
+
}
|
|
1648
|
+
process.stderr.write(`[agent-harness-kit] MCP server starting (stdio)
|
|
1649
|
+
`);
|
|
1650
|
+
await startMcpServer(config, cwd2);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/commands/status.ts
|
|
1654
|
+
import Table from "cli-table3";
|
|
1655
|
+
import pc7 from "picocolors";
|
|
1656
|
+
var STATUS_COLOR = {
|
|
1657
|
+
pending: (s) => pc7.dim(s),
|
|
1658
|
+
in_progress: (s) => pc7.cyan(s),
|
|
1659
|
+
done: (s) => pc7.green(s),
|
|
1660
|
+
blocked: (s) => pc7.red(s)
|
|
1661
|
+
};
|
|
1662
|
+
async function runStatus(cwd2, opts) {
|
|
1663
|
+
const config = await loadConfig(cwd2);
|
|
1664
|
+
const db = openDB(config, cwd2);
|
|
1665
|
+
try {
|
|
1666
|
+
const tasks = db.getTasks();
|
|
1667
|
+
const summary = db.getStatusSummary();
|
|
1668
|
+
if (opts.json) {
|
|
1669
|
+
const actions = tasks.map((t) => ({
|
|
1670
|
+
...t,
|
|
1671
|
+
actions: db.getActionsForTask(t.id),
|
|
1672
|
+
acceptance: db.getTaskAcceptance(t.id)
|
|
1673
|
+
}));
|
|
1674
|
+
console.log(JSON.stringify({ tasks: actions, summary }, null, 2));
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (tasks.length === 0) {
|
|
1678
|
+
console.log(pc7.dim("No tasks yet. Run: ahk task add"));
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const table = new Table({
|
|
1682
|
+
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc7.bold(h)),
|
|
1683
|
+
style: { head: [], border: [] }
|
|
1684
|
+
});
|
|
1685
|
+
for (const t of tasks) {
|
|
1686
|
+
const colorFn = STATUS_COLOR[t.status] ?? ((s) => s);
|
|
1687
|
+
table.push([
|
|
1688
|
+
String(t.id),
|
|
1689
|
+
t.slug,
|
|
1690
|
+
t.title.slice(0, 40),
|
|
1691
|
+
colorFn(t.status),
|
|
1692
|
+
t.assigned_to ?? "\u2014",
|
|
1693
|
+
t.started_at ? t.started_at.slice(0, 10) : "\u2014"
|
|
1694
|
+
]);
|
|
1695
|
+
}
|
|
1696
|
+
console.log(table.toString());
|
|
1697
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
1698
|
+
if (inProgress.length > 0) {
|
|
1699
|
+
console.log("");
|
|
1700
|
+
console.log(pc7.bold("Active actions:"));
|
|
1701
|
+
for (const t of inProgress) {
|
|
1702
|
+
const actions = db.getActionsForTask(t.id);
|
|
1703
|
+
const active = actions.filter((a) => a.status === "in_progress");
|
|
1704
|
+
for (const a of active) {
|
|
1705
|
+
console.log(` ${pc7.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
console.log("");
|
|
1710
|
+
const parts = summary.map((s) => {
|
|
1711
|
+
const fn = STATUS_COLOR[s.status] ?? ((x) => x);
|
|
1712
|
+
return `${fn(s.status)}: ${s.total}`;
|
|
1713
|
+
});
|
|
1714
|
+
console.log(pc7.dim("Tasks \u2014 ") + parts.join(pc7.dim(" | ")));
|
|
1715
|
+
} finally {
|
|
1716
|
+
db.close();
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/commands/sync.ts
|
|
1721
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1722
|
+
import { join as join11, resolve as resolve8 } from "path";
|
|
1723
|
+
import pc8 from "picocolors";
|
|
1724
|
+
async function runSync(cwd2, opts) {
|
|
1725
|
+
const config = await loadConfig(cwd2);
|
|
1726
|
+
const direction = opts.direction ?? "both";
|
|
1727
|
+
const featureListPath = resolve8(join11(cwd2, config.storage.dir, "feature_list.json"));
|
|
1728
|
+
const db = openDB(config, cwd2);
|
|
1729
|
+
try {
|
|
1730
|
+
if (direction === "in" || direction === "both") {
|
|
1731
|
+
await syncIn(featureListPath, db, opts.dryRun ?? false);
|
|
1732
|
+
}
|
|
1733
|
+
if (direction === "out" || direction === "both") {
|
|
1734
|
+
syncOut(db, cwd2, opts.dryRun ?? false);
|
|
1735
|
+
}
|
|
1736
|
+
} finally {
|
|
1737
|
+
db.close();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async function syncIn(featureListPath, db, dryRun) {
|
|
1741
|
+
if (!existsSync7(featureListPath)) {
|
|
1742
|
+
console.log(pc8.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
let seeds;
|
|
1746
|
+
try {
|
|
1747
|
+
seeds = JSON.parse(readFileSync6(featureListPath, "utf8"));
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
console.error(pc8.red(`Failed to parse feature_list.json: ${err}`));
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
if (dryRun) {
|
|
1753
|
+
console.log(pc8.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
1754
|
+
for (const t of seeds) {
|
|
1755
|
+
const existing = db.getTaskBySlug(t.slug);
|
|
1756
|
+
console.log(` ${existing ? pc8.dim("skip") : pc8.green("add ")} ${t.slug}`);
|
|
1757
|
+
}
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const result = db.syncFromFeatureList(seeds);
|
|
1761
|
+
console.log(pc8.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
1762
|
+
}
|
|
1763
|
+
function syncOut(db, cwd2, dryRun) {
|
|
1764
|
+
if (dryRun) {
|
|
1765
|
+
const tasks = db.getTasks();
|
|
1766
|
+
console.log(pc8.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
1767
|
+
console.log(` ${tasks.length} tasks would be written`);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
db.writeFeatureList(cwd2);
|
|
1771
|
+
console.log(pc8.green("\u2713 Out-sync: feature_list.json updated"));
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/commands/task/add.ts
|
|
1775
|
+
import * as p4 from "@clack/prompts";
|
|
1776
|
+
import pc9 from "picocolors";
|
|
1777
|
+
async function runTaskAdd(cwd2) {
|
|
1778
|
+
p4.intro(pc9.bold("agent-harness-kit \u2014 add task"));
|
|
1779
|
+
const titleVal = await p4.text({
|
|
1780
|
+
message: "Task title",
|
|
1781
|
+
validate: (v) => v.trim() ? void 0 : "Title is required"
|
|
1782
|
+
});
|
|
1783
|
+
if (p4.isCancel(titleVal)) {
|
|
1784
|
+
p4.cancel("Cancelled.");
|
|
1785
|
+
process.exit(0);
|
|
1786
|
+
}
|
|
1787
|
+
const title = titleVal.trim();
|
|
1788
|
+
const descVal = await p4.text({
|
|
1789
|
+
message: "Description (what and why)",
|
|
1790
|
+
placeholder: "Optional"
|
|
1791
|
+
});
|
|
1792
|
+
if (p4.isCancel(descVal)) {
|
|
1793
|
+
p4.cancel("Cancelled.");
|
|
1794
|
+
process.exit(0);
|
|
1795
|
+
}
|
|
1796
|
+
const description = descVal.trim();
|
|
1797
|
+
const acceptance = [];
|
|
1798
|
+
p4.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
1799
|
+
while (true) {
|
|
1800
|
+
const val = await p4.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
|
|
1801
|
+
if (p4.isCancel(val) || !val || !val.trim()) break;
|
|
1802
|
+
acceptance.push(val.trim());
|
|
1803
|
+
}
|
|
1804
|
+
const spinner5 = p4.spinner();
|
|
1805
|
+
spinner5.start("Saving...");
|
|
1806
|
+
try {
|
|
1807
|
+
const config = await loadConfig(cwd2);
|
|
1808
|
+
const db = openDB(config, cwd2);
|
|
1809
|
+
const slug = slugify(title);
|
|
1810
|
+
const task2 = db.addTask({ slug, title, description: description || void 0, acceptance });
|
|
1811
|
+
db.writeFeatureList(cwd2);
|
|
1812
|
+
db.close();
|
|
1813
|
+
spinner5.stop("");
|
|
1814
|
+
console.log(pc9.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
|
|
1815
|
+
console.log(pc9.cyan("\u2192") + " " + pc9.cyan("ahk status") + " to see all tasks");
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
spinner5.stop(pc9.red("Failed"));
|
|
1818
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1819
|
+
process.exit(1);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/commands/task/done.ts
|
|
1824
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1825
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1826
|
+
import { resolve as resolve9 } from "path";
|
|
1827
|
+
import pc10 from "picocolors";
|
|
1828
|
+
async function runTaskDone(cwd2, idOrSlug) {
|
|
1829
|
+
const config = await loadConfig(cwd2);
|
|
1830
|
+
if (config.health.required) {
|
|
1831
|
+
const scriptPath = resolve9(cwd2, config.health.scriptPath);
|
|
1832
|
+
if (existsSync8(scriptPath)) {
|
|
1833
|
+
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
1834
|
+
if (result.status !== 0) {
|
|
1835
|
+
console.error(pc10.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
1836
|
+
if (result.stdout) console.error(result.stdout);
|
|
1837
|
+
if (result.stderr) console.error(result.stderr);
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const db = openDB(config, cwd2);
|
|
1843
|
+
try {
|
|
1844
|
+
const parsed = parseInt(idOrSlug, 10);
|
|
1845
|
+
const isId = !isNaN(parsed);
|
|
1846
|
+
const task2 = isId ? db.getTaskById(parsed) : db.getTaskBySlug(idOrSlug);
|
|
1847
|
+
if (!task2) {
|
|
1848
|
+
console.error(pc10.red(`Task not found: ${idOrSlug}`));
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
if (task2.status === "done") {
|
|
1852
|
+
console.log(pc10.dim(`Task #${task2.id} is already done.`));
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
db.updateTaskStatus(task2.id, "done");
|
|
1856
|
+
db.writeFeatureList(cwd2);
|
|
1857
|
+
console.log(pc10.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
1858
|
+
} finally {
|
|
1859
|
+
db.close();
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// src/commands/task/list.ts
|
|
1864
|
+
import Table2 from "cli-table3";
|
|
1865
|
+
import pc11 from "picocolors";
|
|
1866
|
+
var STATUS_COLOR2 = {
|
|
1867
|
+
pending: (s) => pc11.dim(s),
|
|
1868
|
+
in_progress: (s) => pc11.cyan(s),
|
|
1869
|
+
done: (s) => pc11.green(s),
|
|
1870
|
+
blocked: (s) => pc11.red(s)
|
|
1871
|
+
};
|
|
1872
|
+
async function runTaskList(cwd2, opts) {
|
|
1873
|
+
const config = await loadConfig(cwd2);
|
|
1874
|
+
const db = openDB(config, cwd2);
|
|
1875
|
+
try {
|
|
1876
|
+
const validStatuses = ["pending", "in_progress", "done", "blocked"];
|
|
1877
|
+
const filterStatus = opts.status && validStatuses.includes(opts.status) ? opts.status : void 0;
|
|
1878
|
+
const tasks = filterStatus ? db.getTasks(filterStatus) : db.getTasks();
|
|
1879
|
+
if (opts.json) {
|
|
1880
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (tasks.length === 0) {
|
|
1884
|
+
console.log(pc11.dim("No tasks" + (filterStatus ? ` with status: ${filterStatus}` : "") + "."));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const table = new Table2({
|
|
1888
|
+
head: ["ID", "Slug", "Title", "Status"].map((h) => pc11.bold(h)),
|
|
1889
|
+
style: { head: [], border: [] }
|
|
1890
|
+
});
|
|
1891
|
+
for (const t of tasks) {
|
|
1892
|
+
const colorFn = STATUS_COLOR2[t.status] ?? ((s) => s);
|
|
1893
|
+
table.push([String(t.id), t.slug, t.title.slice(0, 50), colorFn(t.status)]);
|
|
1894
|
+
}
|
|
1895
|
+
console.log(table.toString());
|
|
1896
|
+
} finally {
|
|
1897
|
+
db.close();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// src/core/package-data.ts
|
|
1902
|
+
import { createRequire as createRequire2 } from "module";
|
|
1903
|
+
var require2 = createRequire2(import.meta.url);
|
|
1904
|
+
var pkg = require2("@/../package.json");
|
|
1905
|
+
|
|
1906
|
+
// src/core/update-check.ts
|
|
1907
|
+
import pc12 from "picocolors";
|
|
1908
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
1909
|
+
var TIMEOUT_MS = 2500;
|
|
1910
|
+
function checkForUpdate(currentVersion) {
|
|
1911
|
+
return new Promise((resolve10) => {
|
|
1912
|
+
const timer = setTimeout(() => resolve10(null), TIMEOUT_MS);
|
|
1913
|
+
fetch(REGISTRY_URL).then((res) => res.json()).then((data) => {
|
|
1914
|
+
clearTimeout(timer);
|
|
1915
|
+
const latest = data.version;
|
|
1916
|
+
resolve10(isNewer(latest, currentVersion) ? { current: currentVersion, latest } : null);
|
|
1917
|
+
}).catch(() => {
|
|
1918
|
+
clearTimeout(timer);
|
|
1919
|
+
resolve10(null);
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
function printUpdateMessage({ current, latest }) {
|
|
1924
|
+
const lines = [
|
|
1925
|
+
` Update available ${pc12.dim(current)} \u2192 ${pc12.green(latest)} `,
|
|
1926
|
+
` Run: ${pc12.cyan(`npm i -g ${pkg.name}@latest`)} `
|
|
1927
|
+
];
|
|
1928
|
+
const width = Math.max(...lines.map((l) => stripAnsi(l).length));
|
|
1929
|
+
const border = "\u2500".repeat(width);
|
|
1930
|
+
console.log();
|
|
1931
|
+
console.log(pc12.yellow(`\u250C${border}\u2510`));
|
|
1932
|
+
for (const line of lines) {
|
|
1933
|
+
const pad = width - stripAnsi(line).length;
|
|
1934
|
+
console.log(pc12.yellow("\u2502") + line + " ".repeat(pad) + pc12.yellow("\u2502"));
|
|
1935
|
+
}
|
|
1936
|
+
console.log(pc12.yellow(`\u2514${border}\u2518`));
|
|
1937
|
+
console.log();
|
|
1938
|
+
}
|
|
1939
|
+
function isNewer(latest, current) {
|
|
1940
|
+
const toNum = (v) => v.split(".").map(Number);
|
|
1941
|
+
const [lMaj, lMin, lPat] = toNum(latest);
|
|
1942
|
+
const [cMaj, cMin, cPat] = toNum(current);
|
|
1943
|
+
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
1944
|
+
if (lMin !== cMin) return lMin > cMin;
|
|
1945
|
+
return lPat > cPat;
|
|
1946
|
+
}
|
|
1947
|
+
function stripAnsi(str2) {
|
|
1948
|
+
return str2.replace(/\x1B\[[0-9;]*m/g, "");
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/cli.ts
|
|
1952
|
+
var cwd = process.cwd();
|
|
1953
|
+
var updateCheck = checkForUpdate(pkg.version);
|
|
1954
|
+
var program = new Command();
|
|
1955
|
+
program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
|
|
1956
|
+
program.command("init").description("Scaffold a harness interactively in the current directory").option("--name <name>", "Project name (skip prompt)").option("--provider <provider>", "AI provider: claude-code | opencode (skip prompt)").option("--docs <path>", "Docs folder path (skip prompt)").option("--tasks <adapter>", "Task adapter: local | jira | linear (skip prompt)").action(async (opts) => {
|
|
1957
|
+
await runInit(cwd, opts);
|
|
32
1958
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.command('build')
|
|
36
|
-
.description('Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts')
|
|
37
|
-
.option('--watch', 'Rebuild on config changes')
|
|
38
|
-
.action(async (opts) => {
|
|
39
|
-
await runBuild(cwd, opts);
|
|
1959
|
+
program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").action(async (opts) => {
|
|
1960
|
+
await runBuild(cwd, opts);
|
|
40
1961
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.command('health')
|
|
44
|
-
.description('Run health.sh and report result')
|
|
45
|
-
.action(async () => {
|
|
46
|
-
await runHealth(cwd);
|
|
1962
|
+
program.command("health").description("Run health.sh and report result").action(async () => {
|
|
1963
|
+
await runHealth(cwd);
|
|
47
1964
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.command('status')
|
|
51
|
-
.description('Show task table and active actions')
|
|
52
|
-
.option('--json', 'Output as JSON')
|
|
53
|
-
.action(async (opts) => {
|
|
54
|
-
await runStatus(cwd, opts);
|
|
1965
|
+
program.command("status").description("Show task table and active actions").option("--json", "Output as JSON").action(async (opts) => {
|
|
1966
|
+
await runStatus(cwd, opts);
|
|
55
1967
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.command('sync')
|
|
59
|
-
.description('Sync feature_list.json ↔ SQLite')
|
|
60
|
-
.option('--dry-run', 'Show what would change without applying')
|
|
61
|
-
.option('--direction <direction>', 'in | out | both (default: both)')
|
|
62
|
-
.action(async (opts) => {
|
|
63
|
-
await runSync(cwd, { dryRun: opts['dry-run'], direction: opts.direction });
|
|
1968
|
+
program.command("sync").description("Sync feature_list.json \u2194 SQLite").option("--dry-run", "Show what would change without applying").option("--direction <direction>", "in | out | both (default: both)").action(async (opts) => {
|
|
1969
|
+
await runSync(cwd, { dryRun: opts["dry-run"], direction: opts.direction });
|
|
64
1970
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.command('serve')
|
|
68
|
-
.description('Start the MCP server (stdio)')
|
|
69
|
-
.option('--port <port>', 'Port hint stored in config (default: 3742)', parseInt)
|
|
70
|
-
.action(async (opts) => {
|
|
71
|
-
await runServe(cwd, { port: opts.port });
|
|
1971
|
+
program.command("serve").description("Start the MCP server (stdio)").option("--port <port>", "Port hint stored in config (default: 3742)", parseInt).action(async (opts) => {
|
|
1972
|
+
await runServe(cwd, { port: opts.port });
|
|
72
1973
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.command('add')
|
|
77
|
-
.description('Add a task interactively')
|
|
78
|
-
.action(async () => {
|
|
79
|
-
await runTaskAdd(cwd);
|
|
1974
|
+
var task = program.command("task").description("Manage tasks");
|
|
1975
|
+
task.command("add").description("Add a task interactively").action(async () => {
|
|
1976
|
+
await runTaskAdd(cwd);
|
|
80
1977
|
});
|
|
81
|
-
task
|
|
82
|
-
|
|
83
|
-
.description('List tasks')
|
|
84
|
-
.option('--status <status>', 'Filter by status: pending | in_progress | done | blocked')
|
|
85
|
-
.option('--json', 'Output as JSON')
|
|
86
|
-
.action(async (opts) => {
|
|
87
|
-
await runTaskList(cwd, opts);
|
|
1978
|
+
task.command("list").description("List tasks").option("--status <status>", "Filter by status: pending | in_progress | done | blocked").option("--json", "Output as JSON").action(async (opts) => {
|
|
1979
|
+
await runTaskList(cwd, opts);
|
|
88
1980
|
});
|
|
89
|
-
task
|
|
90
|
-
|
|
91
|
-
.description('Mark a task as done')
|
|
92
|
-
.action(async (idOrSlug) => {
|
|
93
|
-
await runTaskDone(cwd, idOrSlug);
|
|
1981
|
+
task.command("done <id|slug>").description("Mark a task as done").action(async (idOrSlug) => {
|
|
1982
|
+
await runTaskDone(cwd, idOrSlug);
|
|
94
1983
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.command('dashboard')
|
|
98
|
-
.description('Open web dashboard to visualize harness data')
|
|
99
|
-
.option('-p, --port <port>', 'Port to listen on', '4242')
|
|
100
|
-
.option('--no-open', 'Do not open browser automatically')
|
|
101
|
-
.action(async (opts) => {
|
|
102
|
-
await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
|
|
1984
|
+
program.command("dashboard").description("Open web dashboard to visualize harness data").option("-p, --port <port>", "Port to listen on", "4242").option("--no-open", "Do not open browser automatically").action(async (opts) => {
|
|
1985
|
+
await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
|
|
103
1986
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.command('migrate')
|
|
107
|
-
.description('Migrate provider-specific files to a different provider')
|
|
108
|
-
.option('--to <provider>', 'Target provider: claude-code | opencode')
|
|
109
|
-
.action(async (opts) => {
|
|
110
|
-
await runMigrate(cwd, opts);
|
|
1987
|
+
program.command("migrate").description("Migrate provider-specific files to a different provider").option("--to <provider>", "Target provider: claude-code | opencode").action(async (opts) => {
|
|
1988
|
+
await runMigrate(cwd, opts);
|
|
111
1989
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.option('--output <path>', 'Output file path (default: stdout)')
|
|
119
|
-
.action(async (opts) => {
|
|
120
|
-
await runExport(cwd, opts);
|
|
1990
|
+
program.command("export").description("Export the database").option("--sql", "SQL dump").option("--json", "JSON export of tasks and actions").option("--output <path>", "Output file path (default: stdout)").action(async (opts) => {
|
|
1991
|
+
await runExport(cwd, opts);
|
|
1992
|
+
});
|
|
1993
|
+
program.hook("postAction", async () => {
|
|
1994
|
+
const update = await updateCheck;
|
|
1995
|
+
if (update) printUpdateMessage(update);
|
|
121
1996
|
});
|
|
122
1997
|
program.parse();
|
|
123
1998
|
//# sourceMappingURL=cli.js.map
|