@cardor/agent-harness-kit 0.17.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -16
- package/dist/agent-templates/lead.md +24 -7
- package/dist/{chunk-LQ7SDMK6.js → chunk-X7FOJOZB.js} +2 -2
- package/dist/chunk-X7FOJOZB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1005 -711
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1 -1
- package/dist/mysql-IMDWH2CU.js +161 -0
- package/dist/mysql-IMDWH2CU.js.map +1 -0
- package/dist/postgres-TYINLEAT.js +148 -0
- package/dist/postgres-TYINLEAT.js.map +1 -0
- package/dist/sqlite-5R6LB3RX.js +143 -0
- package/dist/sqlite-5R6LB3RX.js.map +1 -0
- package/package.json +6 -2
- package/dist/chunk-LQ7SDMK6.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadConfig
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-X7FOJOZB.js";
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
6
|
import { Command } from "commander";
|
|
@@ -155,6 +155,7 @@ actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool ca
|
|
|
155
155
|
actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
|
|
156
156
|
actions.complete actionId summary \u2192 close the action
|
|
157
157
|
actions.get taskId \u2192 full action history for a task
|
|
158
|
+
tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
|
|
158
159
|
tasks.get [status] \u2192 list tasks (pending | in_progress | done | blocked)
|
|
159
160
|
tasks.claim id \u2192 atomically claim a pending task
|
|
160
161
|
tasks.update id status \u2192 change task status
|
|
@@ -199,6 +200,90 @@ If orchestrating: Agent definition files in your provider's agents directory
|
|
|
199
200
|
\`\`\`
|
|
200
201
|
`;
|
|
201
202
|
}
|
|
203
|
+
function claudeMd(config) {
|
|
204
|
+
const { name, description, docsPath } = config.project;
|
|
205
|
+
const port = config.tools.mcp.port;
|
|
206
|
+
return `# CLAUDE.md \u2014 ${name}
|
|
207
|
+
|
|
208
|
+
> **Read this file first.** It is the navigation map for every AI agent working in this repository.
|
|
209
|
+
|
|
210
|
+
## Project
|
|
211
|
+
|
|
212
|
+
**${name}** \u2014 ${description}
|
|
213
|
+
|
|
214
|
+
## Health check (run before starting)
|
|
215
|
+
|
|
216
|
+
\`\`\`bash
|
|
217
|
+
bash health.sh
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
If it exits non-zero, stop and report the issue. Do not proceed with tasks until health is green.
|
|
221
|
+
|
|
222
|
+
## Harness data (source of truth)
|
|
223
|
+
|
|
224
|
+
| File | Purpose |
|
|
225
|
+
|------|---------|
|
|
226
|
+
| \`.harness/harness.db\` | SQLite: all tasks, actions, file changes, tool calls |
|
|
227
|
+
| \`.harness/current.md\` | Markdown fallback \u2014 read this if MCP server is unavailable |
|
|
228
|
+
| \`.harness/feature_list.json\` | Human-editable task seed list |
|
|
229
|
+
|
|
230
|
+
## MCP tools (preferred)
|
|
231
|
+
|
|
232
|
+
The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
|
|
233
|
+
|
|
234
|
+
\`\`\`
|
|
235
|
+
actions.start taskId agent \u2192 start an action, returns actionId
|
|
236
|
+
actions.write actionId section text \u2192 record a section (result, blockers, ...)
|
|
237
|
+
actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
|
|
238
|
+
actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
|
|
239
|
+
actions.complete actionId summary \u2192 close the action
|
|
240
|
+
actions.get taskId \u2192 full action history for a task
|
|
241
|
+
tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
|
|
242
|
+
tasks.get [status] \u2192 list tasks (pending | in_progress | done | blocked)
|
|
243
|
+
tasks.claim id \u2192 atomically claim a pending task
|
|
244
|
+
tasks.update id status \u2192 change task status
|
|
245
|
+
tasks.acceptance.update criterionId \u2192 mark an acceptance criterion as met
|
|
246
|
+
docs.search query \u2192 search ${docsPath} for relevant content
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
## Workflow
|
|
250
|
+
|
|
251
|
+
\`\`\`
|
|
252
|
+
1. INIT
|
|
253
|
+
- Run health.sh \u2192 exit 1 means stop
|
|
254
|
+
- tasks.get('in_progress') \u2192 resume if something is in progress
|
|
255
|
+
- tasks.get('pending') \u2192 pick lowest id
|
|
256
|
+
- No pending tasks? \u2192 ask user, infer fields, call tasks.add, then tasks.claim
|
|
257
|
+
|
|
258
|
+
2. WORK (lead \u2192 explorer \u2192 builder \u2192 reviewer)
|
|
259
|
+
- Each agent calls actions.start(taskId, agentName) \u2192 actionId
|
|
260
|
+
- After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
|
|
261
|
+
- After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
|
|
262
|
+
- Closes with actions.complete(actionId, summary)
|
|
263
|
+
|
|
264
|
+
3. CLOSE
|
|
265
|
+
- tasks.update(taskId, 'done')
|
|
266
|
+
- Run health.sh \u2192 must be green before closing
|
|
267
|
+
\`\`\`
|
|
268
|
+
|
|
269
|
+
## Agent roles
|
|
270
|
+
|
|
271
|
+
| Agent | Responsibility |
|
|
272
|
+
|-------|---------------|
|
|
273
|
+
| lead | Decomposes the task into a plan, assigns sub-agents |
|
|
274
|
+
| explorer | Reads and maps relevant code, never writes |
|
|
275
|
+
| builder | Implements the plan, writes files |
|
|
276
|
+
| reviewer | Verifies acceptance criteria, approves or blocks |
|
|
277
|
+
|
|
278
|
+
## What to read
|
|
279
|
+
|
|
280
|
+
\`\`\`
|
|
281
|
+
Always: .harness/current.md (or MCP tasks.get)
|
|
282
|
+
If implementing: ${docsPath}/
|
|
283
|
+
If orchestrating: Agent definition files in .claude/agents/
|
|
284
|
+
\`\`\`
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
202
287
|
function configTs(params) {
|
|
203
288
|
return `import { defineHarness } from '@cardor/agent-harness-kit'
|
|
204
289
|
|
|
@@ -219,9 +304,13 @@ export default defineHarness({
|
|
|
219
304
|
custom: [],
|
|
220
305
|
},
|
|
221
306
|
|
|
307
|
+
// SQLite (default). Switch to postgres/mysql by changing database.type.
|
|
308
|
+
// database: { type: 'postgres', connectionString: process.env.DATABASE_URL },
|
|
309
|
+
// database: { type: 'mysql', connectionString: process.env.DATABASE_URL },
|
|
310
|
+
database: { type: 'sqlite', path: '.harness/harness.db' },
|
|
311
|
+
|
|
222
312
|
storage: {
|
|
223
313
|
dir: '.harness',
|
|
224
|
-
dbPath: '.harness/harness.db',
|
|
225
314
|
tasks: { adapter: '${params.tasksAdapter}' },
|
|
226
315
|
sections: {
|
|
227
316
|
toolsUsed: true,
|
|
@@ -297,6 +386,7 @@ var ClaudeCodeMaterializer = class {
|
|
|
297
386
|
writeFileSync3(abs, content, { encoding: "utf8", mode });
|
|
298
387
|
};
|
|
299
388
|
write("AGENTS.md", agentsMd(config));
|
|
389
|
+
write("CLAUDE.md", claudeMd(config));
|
|
300
390
|
if (!existsSync3(join3(cwd2, "health.sh"))) {
|
|
301
391
|
write("health.sh", HEALTH_SH, 493);
|
|
302
392
|
}
|
|
@@ -331,6 +421,7 @@ No tasks in progress.
|
|
|
331
421
|
writeFileSync3(abs, content, "utf8");
|
|
332
422
|
};
|
|
333
423
|
write("AGENTS.md", agentsMd(config));
|
|
424
|
+
write("CLAUDE.md", claudeMd(config));
|
|
334
425
|
const projectName = config.project.name;
|
|
335
426
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
336
427
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
@@ -463,7 +554,6 @@ import { extname, join as join5 } from "path";
|
|
|
463
554
|
import { serve } from "@hono/node-server";
|
|
464
555
|
import { Hono } from "hono";
|
|
465
556
|
import { WebSocketServer } from "ws";
|
|
466
|
-
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
467
557
|
var MIME = {
|
|
468
558
|
".html": "text/html; charset=utf-8",
|
|
469
559
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -486,130 +576,54 @@ function fileResponse(filePath) {
|
|
|
486
576
|
}
|
|
487
577
|
function startDashboardServer(db, dbPath, staticPath, port) {
|
|
488
578
|
const app = new Hono();
|
|
579
|
+
const { tasks, actions, stats } = db;
|
|
489
580
|
app.use("/api/*", async (c, next) => {
|
|
490
581
|
await next();
|
|
491
582
|
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
492
583
|
});
|
|
493
|
-
app.get("/api/stats", (c) => {
|
|
494
|
-
const summary =
|
|
584
|
+
app.get("/api/stats", async (c) => {
|
|
585
|
+
const summary = await tasks.getStatusSummary();
|
|
495
586
|
const byStatus = { pending: 0, in_progress: 0, done: 0, blocked: 0 };
|
|
496
587
|
for (const { status, total } of summary) byStatus[status] = total;
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
const [{ total: uniqueTools }] = db.queryRaw(`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`);
|
|
500
|
-
const [{ total: activeAgents }] = db.queryRaw(
|
|
501
|
-
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
502
|
-
);
|
|
503
|
-
return c.json({ byStatus, totalActions, totalFiles, uniqueTools, activeAgents });
|
|
588
|
+
const counts = await stats.getCounts();
|
|
589
|
+
return c.json({ byStatus, ...counts });
|
|
504
590
|
});
|
|
505
591
|
app.get("/api/meta", (c) => {
|
|
506
592
|
return c.json({ ok: true });
|
|
507
593
|
});
|
|
508
|
-
app.get("/api/tasks", (c) => {
|
|
509
|
-
|
|
510
|
-
SELECT t.*,
|
|
511
|
-
COUNT(ta.id) as acceptance_total,
|
|
512
|
-
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
513
|
-
FROM tasks t
|
|
514
|
-
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
515
|
-
GROUP BY t.id
|
|
516
|
-
ORDER BY t.id
|
|
517
|
-
`);
|
|
518
|
-
return c.json(rows);
|
|
594
|
+
app.get("/api/tasks", async (c) => {
|
|
595
|
+
return c.json(await tasks.getAllWithAcceptanceCounts());
|
|
519
596
|
});
|
|
520
|
-
app.get("/api/tasks/:id", (c) => {
|
|
597
|
+
app.get("/api/tasks/:id", async (c) => {
|
|
521
598
|
const id = parseInt(c.req.param("id"));
|
|
522
|
-
const task2 =
|
|
599
|
+
const task2 = await tasks.getById(id);
|
|
523
600
|
if (!task2) return c.json({ error: "Not found" }, 404);
|
|
524
|
-
const acceptance =
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
sections: db.getActionSections(action.id),
|
|
528
|
-
files: db.queryRaw(`SELECT * FROM action_files WHERE action_id = ?`, action.id),
|
|
529
|
-
tools: db.queryRaw(`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`, action.id)
|
|
530
|
-
}));
|
|
531
|
-
return c.json({ ...task2, acceptance, actions });
|
|
601
|
+
const acceptance = await tasks.getAcceptance(id);
|
|
602
|
+
const taskActions = await actions.getWithDetails(id);
|
|
603
|
+
return c.json({ ...task2, acceptance, actions: taskActions });
|
|
532
604
|
});
|
|
533
|
-
app.get("/api/tools/top", (c) => {
|
|
605
|
+
app.get("/api/tools/top", async (c) => {
|
|
534
606
|
const limit = parseInt(c.req.query("limit") ?? "20");
|
|
535
|
-
return c.json(
|
|
607
|
+
return c.json(await actions.getTopTools(limit));
|
|
536
608
|
});
|
|
537
|
-
app.get("/api/tools/recent", (c) => {
|
|
609
|
+
app.get("/api/tools/recent", async (c) => {
|
|
538
610
|
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
539
|
-
|
|
540
|
-
SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
541
|
-
FROM action_tools at
|
|
542
|
-
JOIN actions a ON at.action_id = a.id
|
|
543
|
-
JOIN tasks t ON a.task_id = t.id
|
|
544
|
-
ORDER BY at.called_at DESC
|
|
545
|
-
LIMIT ?
|
|
546
|
-
`, limit);
|
|
547
|
-
return c.json(rows);
|
|
611
|
+
return c.json(await stats.getRecentTools(limit));
|
|
548
612
|
});
|
|
549
|
-
app.get("/api/files/top", (c) => {
|
|
613
|
+
app.get("/api/files/top", async (c) => {
|
|
550
614
|
const limit = parseInt(c.req.query("limit") ?? "20");
|
|
551
|
-
|
|
552
|
-
SELECT
|
|
553
|
-
file_path,
|
|
554
|
-
COUNT(*) as total,
|
|
555
|
-
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
556
|
-
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
557
|
-
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
558
|
-
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
559
|
-
FROM action_files
|
|
560
|
-
GROUP BY file_path
|
|
561
|
-
ORDER BY total DESC
|
|
562
|
-
LIMIT ?
|
|
563
|
-
`, limit);
|
|
564
|
-
return c.json(rows);
|
|
615
|
+
return c.json(await stats.getTopFiles(limit));
|
|
565
616
|
});
|
|
566
|
-
app.get("/api/files/recent", (c) => {
|
|
617
|
+
app.get("/api/files/recent", async (c) => {
|
|
567
618
|
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
568
|
-
|
|
569
|
-
SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
570
|
-
a.agent, a.created_at as called_at
|
|
571
|
-
FROM action_files af
|
|
572
|
-
JOIN actions a ON af.action_id = a.id
|
|
573
|
-
JOIN tasks t ON a.task_id = t.id
|
|
574
|
-
ORDER BY a.created_at DESC
|
|
575
|
-
LIMIT ?
|
|
576
|
-
`, limit);
|
|
577
|
-
return c.json(rows);
|
|
619
|
+
return c.json(await stats.getRecentFiles(limit));
|
|
578
620
|
});
|
|
579
|
-
app.get("/api/agents/stats", (c) => {
|
|
580
|
-
|
|
581
|
-
SELECT
|
|
582
|
-
a.agent,
|
|
583
|
-
COUNT(*) as actions_total,
|
|
584
|
-
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
585
|
-
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
586
|
-
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
587
|
-
COUNT(DISTINCT af.file_path) as files_touched
|
|
588
|
-
FROM actions a
|
|
589
|
-
LEFT JOIN action_files af ON af.action_id = a.id
|
|
590
|
-
GROUP BY a.agent
|
|
591
|
-
ORDER BY actions_total DESC
|
|
592
|
-
`);
|
|
593
|
-
const sorted = rows.sort((a, b) => {
|
|
594
|
-
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
595
|
-
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
596
|
-
if (ai === -1 && bi === -1) return 0;
|
|
597
|
-
if (ai === -1) return 1;
|
|
598
|
-
if (bi === -1) return -1;
|
|
599
|
-
return ai - bi;
|
|
600
|
-
});
|
|
601
|
-
return c.json(sorted);
|
|
621
|
+
app.get("/api/agents/stats", async (c) => {
|
|
622
|
+
return c.json(await stats.getAgentStats());
|
|
602
623
|
});
|
|
603
|
-
app.get("/api/timeline", (c) => {
|
|
624
|
+
app.get("/api/timeline", async (c) => {
|
|
604
625
|
const limit = parseInt(c.req.query("limit") ?? "50");
|
|
605
|
-
|
|
606
|
-
SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
607
|
-
FROM actions a
|
|
608
|
-
JOIN tasks t ON a.task_id = t.id
|
|
609
|
-
ORDER BY a.created_at DESC
|
|
610
|
-
LIMIT ?
|
|
611
|
-
`, limit);
|
|
612
|
-
return c.json(rows);
|
|
626
|
+
return c.json(await stats.getTimeline(limit));
|
|
613
627
|
});
|
|
614
628
|
app.get("/*", (c) => {
|
|
615
629
|
const urlPath = c.req.path;
|
|
@@ -646,14 +660,17 @@ function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
646
660
|
}
|
|
647
661
|
}, 150);
|
|
648
662
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
663
|
+
let watcher = null;
|
|
664
|
+
if (dbPath) {
|
|
665
|
+
const walPath = `${dbPath}-wal`;
|
|
666
|
+
const watchTarget = existsSync5(walPath) ? walPath : dbPath;
|
|
667
|
+
watcher = watch2(watchTarget, broadcast);
|
|
668
|
+
}
|
|
652
669
|
return {
|
|
653
670
|
url: `http://localhost:${port}`,
|
|
654
671
|
close: () => {
|
|
655
672
|
clearTimeout(debounce);
|
|
656
|
-
watcher
|
|
673
|
+
watcher?.close();
|
|
657
674
|
wss.close();
|
|
658
675
|
httpServer.close();
|
|
659
676
|
}
|
|
@@ -665,264 +682,408 @@ import { randomUUID } from "crypto";
|
|
|
665
682
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
666
683
|
import { dirname as dirname3, join as join6, resolve as resolve4 } from "path";
|
|
667
684
|
|
|
668
|
-
// src/core/
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
685
|
+
// src/core/repositories/ActionRepository.ts
|
|
686
|
+
var ActionRepository = class {
|
|
687
|
+
constructor(driver) {
|
|
688
|
+
this.driver = driver;
|
|
689
|
+
}
|
|
690
|
+
driver;
|
|
691
|
+
async create(id, taskId, agent, now) {
|
|
692
|
+
await this.driver.exec(
|
|
693
|
+
`INSERT INTO actions (id, task_id, agent, status, created_at) VALUES (?, ?, ?, 'in_progress', ?)`,
|
|
694
|
+
[id, taskId, agent, now]
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
async complete(actionId, summary, now) {
|
|
698
|
+
await this.driver.exec(
|
|
699
|
+
`UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,
|
|
700
|
+
[now, summary, actionId]
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
async closeOrphaned(taskId, now) {
|
|
704
|
+
return this.driver.exec(
|
|
705
|
+
`UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,
|
|
706
|
+
[now, taskId]
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
async getById(actionId) {
|
|
710
|
+
return this.driver.queryOne(`SELECT * FROM actions WHERE id = ?`, [actionId]);
|
|
711
|
+
}
|
|
712
|
+
async getForTask(taskId) {
|
|
713
|
+
return this.driver.query(
|
|
714
|
+
`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,
|
|
715
|
+
[taskId]
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
async getAll() {
|
|
719
|
+
return this.driver.query(`SELECT * FROM actions ORDER BY created_at`);
|
|
720
|
+
}
|
|
721
|
+
async getWithDetails(taskId) {
|
|
722
|
+
const actions = await this.getForTask(taskId);
|
|
723
|
+
return Promise.all(
|
|
724
|
+
actions.map(async (action) => ({
|
|
725
|
+
...action,
|
|
726
|
+
sections: await this.getSections(action.id),
|
|
727
|
+
files: await this.getFiles(action.id),
|
|
728
|
+
tools: await this.getTools(action.id)
|
|
729
|
+
}))
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
// ─── Sections ─────────────────────────────────────────────────────────────
|
|
733
|
+
async addSection(actionId, sectionType, content, now) {
|
|
734
|
+
await this.driver.exec(
|
|
735
|
+
`INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,
|
|
736
|
+
[actionId, sectionType, content, now]
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
async getSections(actionId) {
|
|
740
|
+
return this.driver.query(
|
|
741
|
+
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,
|
|
742
|
+
[actionId]
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
async getAllSections() {
|
|
746
|
+
return this.driver.query(`SELECT * FROM action_sections ORDER BY created_at`);
|
|
747
|
+
}
|
|
748
|
+
// ─── Files ────────────────────────────────────────────────────────────────
|
|
749
|
+
async addFile(actionId, filePath, operation, notes) {
|
|
750
|
+
await this.driver.exec(
|
|
751
|
+
`INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,
|
|
752
|
+
[actionId, filePath, operation, notes]
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
async getFiles(actionId) {
|
|
756
|
+
return this.driver.query(
|
|
757
|
+
`SELECT * FROM action_files WHERE action_id = ?`,
|
|
758
|
+
[actionId]
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
async getFilesForTask(taskId) {
|
|
762
|
+
return this.driver.query(
|
|
763
|
+
`SELECT af.*, a.agent FROM action_files af JOIN actions a ON af.action_id = a.id WHERE a.task_id = ? ORDER BY a.agent, af.operation`,
|
|
764
|
+
[taskId]
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
// ─── Tools ────────────────────────────────────────────────────────────────
|
|
768
|
+
async addTool(actionId, toolName, argsJson, resultSummary, now) {
|
|
769
|
+
await this.driver.exec(
|
|
770
|
+
`INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,
|
|
771
|
+
[actionId, toolName, argsJson, resultSummary, now]
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
async getTools(actionId) {
|
|
775
|
+
return this.driver.query(
|
|
776
|
+
`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,
|
|
777
|
+
[actionId]
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
async getTopTools(limit) {
|
|
781
|
+
return this.driver.query(
|
|
782
|
+
`SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,
|
|
783
|
+
[limit]
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
726
787
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
)
|
|
788
|
+
// src/core/repositories/StatsRepository.ts
|
|
789
|
+
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
790
|
+
var StatsRepository = class {
|
|
791
|
+
constructor(driver) {
|
|
792
|
+
this.driver = driver;
|
|
793
|
+
}
|
|
794
|
+
driver;
|
|
795
|
+
async getCounts() {
|
|
796
|
+
const [{ total: totalActions }] = await this.driver.query(
|
|
797
|
+
`SELECT COUNT(*) as total FROM actions`
|
|
798
|
+
);
|
|
799
|
+
const [{ total: totalFiles }] = await this.driver.query(
|
|
800
|
+
`SELECT COUNT(*) as total FROM action_files`
|
|
801
|
+
);
|
|
802
|
+
const [{ total: uniqueTools }] = await this.driver.query(
|
|
803
|
+
`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`
|
|
804
|
+
);
|
|
805
|
+
const [{ total: activeAgents }] = await this.driver.query(
|
|
806
|
+
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
807
|
+
);
|
|
808
|
+
return { totalActions, totalFiles, uniqueTools, activeAgents };
|
|
809
|
+
}
|
|
810
|
+
async getRecentTools(limit) {
|
|
811
|
+
return this.driver.query(
|
|
812
|
+
`SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
813
|
+
FROM action_tools at
|
|
814
|
+
JOIN actions a ON at.action_id = a.id
|
|
815
|
+
JOIN tasks t ON a.task_id = t.id
|
|
816
|
+
ORDER BY at.called_at DESC
|
|
817
|
+
LIMIT ?`,
|
|
818
|
+
[limit]
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
async getTopFiles(limit) {
|
|
822
|
+
return this.driver.query(
|
|
823
|
+
`SELECT
|
|
824
|
+
file_path,
|
|
825
|
+
COUNT(*) as total,
|
|
826
|
+
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
827
|
+
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
828
|
+
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
829
|
+
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
830
|
+
FROM action_files
|
|
831
|
+
GROUP BY file_path
|
|
832
|
+
ORDER BY total DESC
|
|
833
|
+
LIMIT ?`,
|
|
834
|
+
[limit]
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
async getRecentFiles(limit) {
|
|
838
|
+
return this.driver.query(
|
|
839
|
+
`SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
840
|
+
a.agent, a.created_at as called_at
|
|
841
|
+
FROM action_files af
|
|
842
|
+
JOIN actions a ON af.action_id = a.id
|
|
843
|
+
JOIN tasks t ON a.task_id = t.id
|
|
844
|
+
ORDER BY a.created_at DESC
|
|
845
|
+
LIMIT ?`,
|
|
846
|
+
[limit]
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
async getAgentStats() {
|
|
850
|
+
const rows = await this.driver.query(
|
|
851
|
+
`SELECT
|
|
852
|
+
a.agent,
|
|
853
|
+
COUNT(*) as actions_total,
|
|
854
|
+
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
855
|
+
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
856
|
+
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
857
|
+
COUNT(DISTINCT af.file_path) as files_touched
|
|
858
|
+
FROM actions a
|
|
859
|
+
LEFT JOIN action_files af ON af.action_id = a.id
|
|
860
|
+
GROUP BY a.agent
|
|
861
|
+
ORDER BY actions_total DESC`
|
|
862
|
+
);
|
|
863
|
+
return rows.sort((a, b) => {
|
|
864
|
+
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
865
|
+
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
866
|
+
if (ai === -1 && bi === -1) return 0;
|
|
867
|
+
if (ai === -1) return 1;
|
|
868
|
+
if (bi === -1) return -1;
|
|
869
|
+
return ai - bi;
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async getTimeline(limit) {
|
|
873
|
+
return this.driver.query(
|
|
874
|
+
`SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
875
|
+
FROM actions a
|
|
876
|
+
JOIN tasks t ON a.task_id = t.id
|
|
877
|
+
ORDER BY a.created_at DESC
|
|
878
|
+
LIMIT ?`,
|
|
879
|
+
[limit]
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
735
883
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
);
|
|
884
|
+
// src/core/repositories/TaskRepository.ts
|
|
885
|
+
var TaskRepository = class {
|
|
886
|
+
constructor(driver) {
|
|
887
|
+
this.driver = driver;
|
|
888
|
+
}
|
|
889
|
+
driver;
|
|
890
|
+
async add(params) {
|
|
891
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
892
|
+
return this.driver.insert(
|
|
893
|
+
`INSERT INTO tasks (slug, title, description, status, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
894
|
+
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now]
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
async addAcceptance(taskId, criteria) {
|
|
898
|
+
for (const criterion of criteria) {
|
|
899
|
+
await this.driver.exec(
|
|
900
|
+
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
901
|
+
[taskId, criterion]
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async getAll(status) {
|
|
906
|
+
if (status) {
|
|
907
|
+
return this.driver.query(`SELECT * FROM tasks WHERE status = ? ORDER BY id`, [status]);
|
|
908
|
+
}
|
|
909
|
+
return this.driver.query(`SELECT * FROM tasks ORDER BY id`);
|
|
910
|
+
}
|
|
911
|
+
async getAllWithAcceptanceCounts() {
|
|
912
|
+
return this.driver.query(`
|
|
913
|
+
SELECT t.*,
|
|
914
|
+
COUNT(ta.id) as acceptance_total,
|
|
915
|
+
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
916
|
+
FROM tasks t
|
|
917
|
+
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
918
|
+
GROUP BY t.id
|
|
919
|
+
ORDER BY t.id
|
|
920
|
+
`);
|
|
921
|
+
}
|
|
922
|
+
async getById(id) {
|
|
923
|
+
return this.driver.queryOne(`SELECT * FROM tasks WHERE id = ?`, [id]);
|
|
924
|
+
}
|
|
925
|
+
async getBySlug(slug) {
|
|
926
|
+
return this.driver.queryOne(`SELECT * FROM tasks WHERE slug = ?`, [slug]);
|
|
927
|
+
}
|
|
928
|
+
async getAcceptance(taskId) {
|
|
929
|
+
return this.driver.query(
|
|
930
|
+
`SELECT * FROM task_acceptance WHERE task_id = ?`,
|
|
931
|
+
[taskId]
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
async setStatus(id, status, extra) {
|
|
935
|
+
if (extra?.started_at) {
|
|
936
|
+
await this.driver.exec(
|
|
937
|
+
`UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`,
|
|
938
|
+
[status, extra.started_at, id]
|
|
939
|
+
);
|
|
940
|
+
} else if (extra?.completed_at) {
|
|
941
|
+
await this.driver.exec(
|
|
942
|
+
`UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`,
|
|
943
|
+
[status, extra.completed_at, id]
|
|
944
|
+
);
|
|
945
|
+
} else {
|
|
946
|
+
await this.driver.exec(`UPDATE tasks SET status = ? WHERE id = ?`, [status, id]);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async claim(id, agent, now) {
|
|
950
|
+
return this.driver.exec(
|
|
951
|
+
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ? WHERE id = ? AND status = 'pending'`,
|
|
952
|
+
[agent, now, id]
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
async markAcceptanceMet(criterionId) {
|
|
956
|
+
await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId]);
|
|
957
|
+
}
|
|
958
|
+
async getStatusSummary() {
|
|
959
|
+
return this.driver.query(
|
|
960
|
+
`SELECT status, COUNT(*) as total FROM tasks GROUP BY status`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
744
964
|
|
|
745
|
-
|
|
746
|
-
CREATE INDEX IF NOT EXISTS idx_actions_task_id ON actions(task_id);
|
|
747
|
-
CREATE INDEX IF NOT EXISTS idx_actions_agent ON actions(agent);
|
|
748
|
-
CREATE INDEX IF NOT EXISTS idx_actions_status ON actions(status);
|
|
749
|
-
CREATE INDEX IF NOT EXISTS idx_action_files_path ON action_files(file_path);
|
|
750
|
-
CREATE INDEX IF NOT EXISTS idx_action_tools_name ON action_tools(tool_name);
|
|
751
|
-
`;
|
|
965
|
+
// src/core/db.ts
|
|
752
966
|
var HarnessDB = class {
|
|
753
|
-
|
|
967
|
+
tasks;
|
|
968
|
+
actions;
|
|
969
|
+
stats;
|
|
970
|
+
driver;
|
|
754
971
|
config;
|
|
755
|
-
constructor(
|
|
972
|
+
constructor(driver, config) {
|
|
973
|
+
this.driver = driver;
|
|
756
974
|
this.config = config;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
this.
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
// ─── Tasks ────────────────────────────────────────────────────────────────
|
|
765
|
-
addTask(params) {
|
|
766
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
767
|
-
this.db.prepare(
|
|
768
|
-
`INSERT INTO tasks (slug, title, description, status, created_at)
|
|
769
|
-
VALUES (@slug, @title, @description, 'pending', @created_at)`
|
|
770
|
-
).run({
|
|
975
|
+
this.tasks = new TaskRepository(driver);
|
|
976
|
+
this.actions = new ActionRepository(driver);
|
|
977
|
+
this.stats = new StatsRepository(driver);
|
|
978
|
+
}
|
|
979
|
+
// ─── Tasks (public facade — delegates to TaskRepository) ──────────────────
|
|
980
|
+
async addTask(params) {
|
|
981
|
+
const taskId = await this.tasks.add({
|
|
771
982
|
slug: params.slug,
|
|
772
983
|
title: params.title,
|
|
773
|
-
description: params.description
|
|
774
|
-
created_at: now
|
|
984
|
+
description: params.description
|
|
775
985
|
});
|
|
776
|
-
const taskId = lastInsertId(this.db);
|
|
777
986
|
if (params.acceptance?.length) {
|
|
778
|
-
|
|
779
|
-
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`
|
|
780
|
-
);
|
|
781
|
-
for (const criterion of params.acceptance) {
|
|
782
|
-
accStmt.run(taskId, criterion);
|
|
783
|
-
}
|
|
987
|
+
await this.tasks.addAcceptance(taskId, params.acceptance);
|
|
784
988
|
}
|
|
785
|
-
this.regenerateCurrentMd();
|
|
786
|
-
return this.
|
|
989
|
+
await this.regenerateCurrentMd();
|
|
990
|
+
return await this.tasks.getById(taskId);
|
|
787
991
|
}
|
|
788
|
-
getTasks(status) {
|
|
789
|
-
|
|
790
|
-
return this.db.prepare(`SELECT * FROM tasks WHERE status = ? ORDER BY id`).all(status);
|
|
791
|
-
}
|
|
792
|
-
return this.db.prepare(`SELECT * FROM tasks ORDER BY id`).all();
|
|
992
|
+
async getTasks(status) {
|
|
993
|
+
return this.tasks.getAll(status);
|
|
793
994
|
}
|
|
794
|
-
getTaskById(id) {
|
|
795
|
-
return this.
|
|
995
|
+
async getTaskById(id) {
|
|
996
|
+
return this.tasks.getById(id);
|
|
796
997
|
}
|
|
797
|
-
getTaskBySlug(slug) {
|
|
798
|
-
return this.
|
|
998
|
+
async getTaskBySlug(slug) {
|
|
999
|
+
return this.tasks.getBySlug(slug);
|
|
799
1000
|
}
|
|
800
|
-
getTaskAcceptance(taskId) {
|
|
801
|
-
return this.
|
|
1001
|
+
async getTaskAcceptance(taskId) {
|
|
1002
|
+
return this.tasks.getAcceptance(taskId);
|
|
802
1003
|
}
|
|
803
|
-
updateTaskStatus(idOrSlug, status) {
|
|
1004
|
+
async updateTaskStatus(idOrSlug, status) {
|
|
804
1005
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
805
|
-
const task2 = typeof idOrSlug === "number" ? this.
|
|
1006
|
+
const task2 = typeof idOrSlug === "number" ? await this.tasks.getById(idOrSlug) : await this.tasks.getBySlug(idOrSlug);
|
|
806
1007
|
if (!task2) throw new Error(`Task not found: ${idOrSlug}`);
|
|
807
1008
|
if (status === "in_progress" && !task2.started_at) {
|
|
808
|
-
this.
|
|
809
|
-
`UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`
|
|
810
|
-
).run(status, now, task2.id);
|
|
1009
|
+
await this.tasks.setStatus(task2.id, status, { started_at: now });
|
|
811
1010
|
} else if (status === "done") {
|
|
812
|
-
this.
|
|
813
|
-
`UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`
|
|
814
|
-
).run(status, now, task2.id);
|
|
1011
|
+
await this.tasks.setStatus(task2.id, status, { completed_at: now });
|
|
815
1012
|
} else {
|
|
816
|
-
this.
|
|
1013
|
+
await this.tasks.setStatus(task2.id, status);
|
|
817
1014
|
}
|
|
818
|
-
this.regenerateCurrentMd();
|
|
819
|
-
return this.
|
|
1015
|
+
await this.regenerateCurrentMd();
|
|
1016
|
+
return await this.tasks.getById(task2.id);
|
|
820
1017
|
}
|
|
821
|
-
claimTask(id, agent) {
|
|
1018
|
+
async claimTask(id, agent) {
|
|
822
1019
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
823
|
-
this.
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
).run(agent, now, id);
|
|
829
|
-
this.db.exec("COMMIT");
|
|
830
|
-
const task2 = this.getTaskById(id);
|
|
1020
|
+
return this.driver.transaction(async (tx) => {
|
|
1021
|
+
const txTasks = new TaskRepository(tx);
|
|
1022
|
+
const changed = await txTasks.claim(id, agent, now);
|
|
1023
|
+
if (!changed) return null;
|
|
1024
|
+
const task2 = await txTasks.getById(id);
|
|
831
1025
|
if (!task2 || task2.status !== "in_progress" || task2.assigned_to !== agent) return null;
|
|
832
|
-
this.regenerateCurrentMd();
|
|
1026
|
+
await this.regenerateCurrentMd();
|
|
833
1027
|
return task2;
|
|
834
|
-
}
|
|
835
|
-
this.db.exec("ROLLBACK");
|
|
836
|
-
throw err;
|
|
837
|
-
}
|
|
1028
|
+
});
|
|
838
1029
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1030
|
+
async markAcceptanceMet(criterionId) {
|
|
1031
|
+
return this.tasks.markAcceptanceMet(criterionId);
|
|
1032
|
+
}
|
|
1033
|
+
async getStatusSummary() {
|
|
1034
|
+
return this.tasks.getStatusSummary();
|
|
1035
|
+
}
|
|
1036
|
+
// ─── Actions (public facade — delegates to ActionRepository) ──────────────
|
|
1037
|
+
async startAction(taskId, agent) {
|
|
842
1038
|
const id = randomUUID();
|
|
843
|
-
this.db.prepare(
|
|
844
|
-
`INSERT INTO actions (id, task_id, agent, status, created_at)
|
|
845
|
-
VALUES (?, ?, ?, 'in_progress', ?)`
|
|
846
|
-
).run(id, taskId, agent, now);
|
|
847
|
-
this.regenerateCurrentMd();
|
|
848
|
-
return this.getAction(id);
|
|
849
|
-
}
|
|
850
|
-
writeSection(actionId, sectionType, content) {
|
|
851
1039
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
852
|
-
this.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1040
|
+
await this.actions.create(id, taskId, agent, now);
|
|
1041
|
+
await this.regenerateCurrentMd();
|
|
1042
|
+
return await this.actions.getById(id);
|
|
1043
|
+
}
|
|
1044
|
+
async writeSection(actionId, sectionType, content) {
|
|
1045
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
+
await this.actions.addSection(actionId, sectionType, content, now);
|
|
1047
|
+
await this.regenerateCurrentMd();
|
|
857
1048
|
}
|
|
858
|
-
completeAction(actionId, summary) {
|
|
1049
|
+
async completeAction(actionId, summary) {
|
|
859
1050
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
-
this.
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
return this.getAction(actionId);
|
|
866
|
-
}
|
|
867
|
-
closeOrphanedActions(taskId) {
|
|
1051
|
+
await this.actions.complete(actionId, summary, now);
|
|
1052
|
+
await this.regenerateCurrentMd();
|
|
1053
|
+
return await this.actions.getById(actionId);
|
|
1054
|
+
}
|
|
1055
|
+
async closeOrphanedActions(taskId) {
|
|
868
1056
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
).all(actionId);
|
|
885
|
-
}
|
|
886
|
-
recordFile(actionId, filePath, operation, notes) {
|
|
887
|
-
this.db.prepare(
|
|
888
|
-
`INSERT INTO action_files (action_id, file_path, operation, notes)
|
|
889
|
-
VALUES (?, ?, ?, ?)`
|
|
890
|
-
).run(actionId, filePath, operation, notes ?? null);
|
|
891
|
-
}
|
|
892
|
-
recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
1057
|
+
return this.actions.closeOrphaned(taskId, now);
|
|
1058
|
+
}
|
|
1059
|
+
async getAction(actionId) {
|
|
1060
|
+
return this.actions.getById(actionId);
|
|
1061
|
+
}
|
|
1062
|
+
async getActionsForTask(taskId) {
|
|
1063
|
+
return this.actions.getForTask(taskId);
|
|
1064
|
+
}
|
|
1065
|
+
async getActionSections(actionId) {
|
|
1066
|
+
return this.actions.getSections(actionId);
|
|
1067
|
+
}
|
|
1068
|
+
async recordFile(actionId, filePath, operation, notes) {
|
|
1069
|
+
return this.actions.addFile(actionId, filePath, operation, notes ?? null);
|
|
1070
|
+
}
|
|
1071
|
+
async recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
893
1072
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
894
|
-
this.
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
return this.
|
|
901
|
-
`SELECT af.*, a.agent
|
|
902
|
-
FROM action_files af
|
|
903
|
-
JOIN actions a ON af.action_id = a.id
|
|
904
|
-
WHERE a.task_id = ?
|
|
905
|
-
ORDER BY a.agent, af.operation`
|
|
906
|
-
).all(taskId);
|
|
907
|
-
}
|
|
908
|
-
getTopTools(limit = 10) {
|
|
909
|
-
return this.db.prepare(
|
|
910
|
-
`SELECT tool_name, COUNT(*) as uses
|
|
911
|
-
FROM action_tools
|
|
912
|
-
GROUP BY tool_name
|
|
913
|
-
ORDER BY uses DESC
|
|
914
|
-
LIMIT ?`
|
|
915
|
-
).all(limit);
|
|
916
|
-
}
|
|
917
|
-
getStatusSummary() {
|
|
918
|
-
return this.db.prepare(`SELECT status, COUNT(*) as total FROM tasks GROUP BY status`).all();
|
|
1073
|
+
return this.actions.addTool(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
|
|
1074
|
+
}
|
|
1075
|
+
async getFilesForTask(taskId) {
|
|
1076
|
+
return this.actions.getFilesForTask(taskId);
|
|
1077
|
+
}
|
|
1078
|
+
async getTopTools(limit = 10) {
|
|
1079
|
+
return this.actions.getTopTools(limit);
|
|
919
1080
|
}
|
|
920
1081
|
// ─── current.md fallback ──────────────────────────────────────────────────
|
|
921
|
-
regenerateCurrentMd() {
|
|
1082
|
+
async regenerateCurrentMd() {
|
|
922
1083
|
if (!this.config.storage.markdownFallback.enabled) return;
|
|
923
1084
|
const mdPath = resolve4(this.config.storage.markdownFallback.path);
|
|
924
1085
|
mkdirSync5(dirname3(mdPath), { recursive: true });
|
|
925
|
-
const inProgress = this.
|
|
1086
|
+
const inProgress = await this.tasks.getAll("in_progress");
|
|
926
1087
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
927
1088
|
let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
928
1089
|
`;
|
|
@@ -936,7 +1097,7 @@ var HarnessDB = class {
|
|
|
936
1097
|
md += `## No tasks in progress
|
|
937
1098
|
|
|
938
1099
|
`;
|
|
939
|
-
const pending = this.
|
|
1100
|
+
const pending = await this.tasks.getAll("pending");
|
|
940
1101
|
if (pending.length > 0) {
|
|
941
1102
|
md += `### Next pending tasks
|
|
942
1103
|
`;
|
|
@@ -958,15 +1119,15 @@ var HarnessDB = class {
|
|
|
958
1119
|
md += `- **Started:** ${task2.started_at ?? "unknown"}
|
|
959
1120
|
|
|
960
1121
|
`;
|
|
961
|
-
const
|
|
962
|
-
if (
|
|
1122
|
+
const taskActions = await this.actions.getForTask(task2.id);
|
|
1123
|
+
if (taskActions.length > 0) {
|
|
963
1124
|
md += `## Actions this session
|
|
964
1125
|
`;
|
|
965
1126
|
md += `| Agent | Status | Summary | Started |
|
|
966
1127
|
`;
|
|
967
1128
|
md += `|----------|-------------|----------------------------------|-------------|
|
|
968
1129
|
`;
|
|
969
|
-
for (const a of
|
|
1130
|
+
for (const a of taskActions) {
|
|
970
1131
|
const started = a.created_at.slice(11, 16);
|
|
971
1132
|
const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
|
|
972
1133
|
md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
|
|
@@ -975,7 +1136,7 @@ var HarnessDB = class {
|
|
|
975
1136
|
md += `
|
|
976
1137
|
`;
|
|
977
1138
|
}
|
|
978
|
-
const acceptance = this.
|
|
1139
|
+
const acceptance = await this.tasks.getAcceptance(task2.id);
|
|
979
1140
|
if (acceptance.length > 0) {
|
|
980
1141
|
md += `## Acceptance Criteria
|
|
981
1142
|
`;
|
|
@@ -990,63 +1151,77 @@ var HarnessDB = class {
|
|
|
990
1151
|
}
|
|
991
1152
|
writeFileSync5(mdPath, md, "utf8");
|
|
992
1153
|
}
|
|
993
|
-
// ─── Raw query
|
|
994
|
-
queryRaw(sql, ...params) {
|
|
995
|
-
return this.
|
|
1154
|
+
// ─── Raw query escape hatch ───────────────────────────────────────────────
|
|
1155
|
+
async queryRaw(sql, ...params) {
|
|
1156
|
+
return this.driver.query(sql, params);
|
|
996
1157
|
}
|
|
997
1158
|
// ─── Export helpers ───────────────────────────────────────────────────────
|
|
998
|
-
exportJson() {
|
|
1159
|
+
async exportJson() {
|
|
999
1160
|
return {
|
|
1000
|
-
tasks: this.
|
|
1001
|
-
actions: this.
|
|
1002
|
-
sections: this.
|
|
1161
|
+
tasks: await this.tasks.getAll(),
|
|
1162
|
+
actions: await this.actions.getAll(),
|
|
1163
|
+
sections: await this.actions.getAllSections()
|
|
1003
1164
|
};
|
|
1004
1165
|
}
|
|
1005
|
-
close() {
|
|
1006
|
-
this.
|
|
1166
|
+
async close() {
|
|
1167
|
+
await this.driver.close();
|
|
1007
1168
|
}
|
|
1008
1169
|
// ─── feature_list.json sync ───────────────────────────────────────────────
|
|
1009
|
-
syncFromFeatureList(
|
|
1170
|
+
async syncFromFeatureList(seeds) {
|
|
1010
1171
|
let added = 0;
|
|
1011
1172
|
let skipped = 0;
|
|
1012
|
-
for (const t of
|
|
1013
|
-
if (this.
|
|
1173
|
+
for (const t of seeds) {
|
|
1174
|
+
if (await this.tasks.getBySlug(t.slug)) {
|
|
1014
1175
|
skipped++;
|
|
1015
1176
|
continue;
|
|
1016
1177
|
}
|
|
1017
|
-
this.addTask(t);
|
|
1178
|
+
await this.addTask(t);
|
|
1018
1179
|
added++;
|
|
1019
1180
|
}
|
|
1020
1181
|
return { added, skipped };
|
|
1021
1182
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}));
|
|
1183
|
+
async writeFeatureList(cwd2) {
|
|
1184
|
+
const allTasks = await this.tasks.getAll();
|
|
1185
|
+
const list = await Promise.all(
|
|
1186
|
+
allTasks.map(async (t) => ({
|
|
1187
|
+
slug: t.slug,
|
|
1188
|
+
title: t.title,
|
|
1189
|
+
description: t.description ?? void 0,
|
|
1190
|
+
acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),
|
|
1191
|
+
status: t.status
|
|
1192
|
+
}))
|
|
1193
|
+
);
|
|
1034
1194
|
const path = join6(resolve4(cwd2), this.config.storage.dir, "feature_list.json");
|
|
1035
1195
|
mkdirSync5(dirname3(path), { recursive: true });
|
|
1036
1196
|
writeFileSync5(path, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
1037
1197
|
}
|
|
1038
1198
|
};
|
|
1039
|
-
function openDB(config, cwd2) {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1199
|
+
async function openDB(config, cwd2) {
|
|
1200
|
+
const dbConfig = config.database;
|
|
1201
|
+
let driver;
|
|
1202
|
+
if (dbConfig.type === "postgres") {
|
|
1203
|
+
const { PostgresDriver } = await import("./postgres-TYINLEAT.js");
|
|
1204
|
+
driver = new PostgresDriver(dbConfig);
|
|
1205
|
+
} else if (dbConfig.type === "mysql") {
|
|
1206
|
+
const { MySQLDriver } = await import("./mysql-IMDWH2CU.js");
|
|
1207
|
+
driver = new MySQLDriver(dbConfig);
|
|
1208
|
+
} else {
|
|
1209
|
+
const { SQLiteDriver } = await import("./sqlite-5R6LB3RX.js");
|
|
1210
|
+
if (dbConfig.type !== "sqlite") {
|
|
1211
|
+
throw new Error("Invalid database type");
|
|
1212
|
+
}
|
|
1213
|
+
driver = new SQLiteDriver(resolve4(cwd2, dbConfig.path));
|
|
1214
|
+
}
|
|
1215
|
+
await driver.ensureSchema();
|
|
1216
|
+
return new HarnessDB(driver, config);
|
|
1042
1217
|
}
|
|
1043
1218
|
|
|
1044
1219
|
// src/commands/dashboard.ts
|
|
1045
1220
|
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
1046
1221
|
async function runDashboard(cwd2, opts) {
|
|
1047
1222
|
const config = await loadConfig(cwd2);
|
|
1048
|
-
const db = openDB(config, cwd2);
|
|
1049
|
-
const dbPath = resolve5(cwd2, config.
|
|
1223
|
+
const db = await openDB(config, cwd2);
|
|
1224
|
+
const dbPath = config.database.type === "sqlite" ? resolve5(cwd2, config.database.path) : null;
|
|
1050
1225
|
const staticPath = join7(__dirname2, "dashboard-dist");
|
|
1051
1226
|
const { url } = startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1052
1227
|
console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
|
|
@@ -1072,10 +1247,10 @@ async function runExport(cwd2, opts) {
|
|
|
1072
1247
|
process.exit(1);
|
|
1073
1248
|
}
|
|
1074
1249
|
const config = await loadConfig(cwd2);
|
|
1075
|
-
const db = openDB(config, cwd2);
|
|
1250
|
+
const db = await openDB(config, cwd2);
|
|
1076
1251
|
try {
|
|
1077
1252
|
if (opts.json) {
|
|
1078
|
-
const data = db.exportJson();
|
|
1253
|
+
const data = await db.exportJson();
|
|
1079
1254
|
const out = JSON.stringify(data, null, 2) + "\n";
|
|
1080
1255
|
if (opts.output) {
|
|
1081
1256
|
writeFileSync6(opts.output, out, "utf8");
|
|
@@ -1089,7 +1264,7 @@ async function runExport(cwd2, opts) {
|
|
|
1089
1264
|
process.exit(1);
|
|
1090
1265
|
}
|
|
1091
1266
|
} finally {
|
|
1092
|
-
db.close();
|
|
1267
|
+
await db.close();
|
|
1093
1268
|
}
|
|
1094
1269
|
}
|
|
1095
1270
|
|
|
@@ -1112,9 +1287,15 @@ async function runHealth(cwd2) {
|
|
|
1112
1287
|
process.exit(1);
|
|
1113
1288
|
}
|
|
1114
1289
|
let allOk = true;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1290
|
+
let dbOk;
|
|
1291
|
+
if (config.database.type === "sqlite") {
|
|
1292
|
+
const dbPath = resolve6(cwd2, config.database.path);
|
|
1293
|
+
dbOk = existsSync6(dbPath);
|
|
1294
|
+
checkLine("checking DB", dbOk, `${config.database.path} reachable`);
|
|
1295
|
+
} else {
|
|
1296
|
+
dbOk = true;
|
|
1297
|
+
checkLine("checking DB", true, `${config.database.type}://${config.database.connectionString.replace(/:[^:@]*@/, ":***@")} configured`);
|
|
1298
|
+
}
|
|
1118
1299
|
if (!dbOk) allOk = false;
|
|
1119
1300
|
const agentsDir = config.provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
1120
1301
|
const agentNames = ["lead", "explorer", "builder", "reviewer"];
|
|
@@ -1166,12 +1347,62 @@ async function runHealth(cwd2) {
|
|
|
1166
1347
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1167
1348
|
import { homedir } from "os";
|
|
1168
1349
|
import { join as join10 } from "path";
|
|
1169
|
-
import * as
|
|
1350
|
+
import * as p3 from "@clack/prompts";
|
|
1170
1351
|
import pc6 from "picocolors";
|
|
1171
1352
|
|
|
1172
|
-
// src/
|
|
1173
|
-
import
|
|
1174
|
-
|
|
1353
|
+
// src/schema/init.ts
|
|
1354
|
+
import * as v from "valibot";
|
|
1355
|
+
var initNameSchema = v.pipe(
|
|
1356
|
+
v.string(),
|
|
1357
|
+
v.nonEmpty("Project name is required"),
|
|
1358
|
+
v.minLength(3, "Project name must be at least 3 characters"),
|
|
1359
|
+
v.maxLength(50, "Project name must be at most 50 characters"),
|
|
1360
|
+
v.trim()
|
|
1361
|
+
);
|
|
1362
|
+
var initDescriptionSchema = v.pipe(
|
|
1363
|
+
v.string(),
|
|
1364
|
+
v.nonEmpty("Description is required"),
|
|
1365
|
+
v.maxLength(300, "Description must be at most 300 characters"),
|
|
1366
|
+
v.trim()
|
|
1367
|
+
);
|
|
1368
|
+
var initDocsSchema = v.pipe(
|
|
1369
|
+
v.string(),
|
|
1370
|
+
v.nonEmpty("Docs folder path is required"),
|
|
1371
|
+
v.regex(/^[\w\-./]+$/, "Docs folder path can only contain letters, numbers, dashes, underscores, dots, and slashes"),
|
|
1372
|
+
v.trim()
|
|
1373
|
+
);
|
|
1374
|
+
|
|
1375
|
+
// src/schema/task.ts
|
|
1376
|
+
import * as v2 from "valibot";
|
|
1377
|
+
var taskTitleSchema = v2.pipe(
|
|
1378
|
+
v2.string(),
|
|
1379
|
+
v2.nonEmpty("Task title is required"),
|
|
1380
|
+
v2.minLength(3, "Task title must be at least 3 characters"),
|
|
1381
|
+
v2.maxLength(100, "Task title must be at most 100 characters")
|
|
1382
|
+
);
|
|
1383
|
+
var taskDescriptionSchema = v2.pipe(
|
|
1384
|
+
v2.string(),
|
|
1385
|
+
v2.nonEmpty("Description is required"),
|
|
1386
|
+
v2.maxLength(1e3, "Description must be at most 1000 characters")
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
// src/utils/form.ts
|
|
1390
|
+
import * as p2 from "@clack/prompts";
|
|
1391
|
+
import * as v3 from "valibot";
|
|
1392
|
+
var cliFormWithRetry = async (formFn, schema) => {
|
|
1393
|
+
while (true) {
|
|
1394
|
+
const res = await formFn();
|
|
1395
|
+
const result = v3.safeParse(schema, res);
|
|
1396
|
+
if (result.success) return result.output;
|
|
1397
|
+
const messages = result.issues.map((i) => i.message).join(", ");
|
|
1398
|
+
p2.log.error(messages);
|
|
1399
|
+
p2.log.info("Please try again.\n");
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// src/commands/init-helpers.ts
|
|
1404
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1405
|
+
import { join as join9 } from "path";
|
|
1175
1406
|
import pc5 from "picocolors";
|
|
1176
1407
|
function readProjectNameFromPackageJson(cwd2) {
|
|
1177
1408
|
try {
|
|
@@ -1202,9 +1433,9 @@ function applyConfigDefaults(params) {
|
|
|
1202
1433
|
reviewer: { instructionsPath: null },
|
|
1203
1434
|
custom: []
|
|
1204
1435
|
},
|
|
1436
|
+
database: { type: "sqlite", path: ".harness/harness.db" },
|
|
1205
1437
|
storage: {
|
|
1206
1438
|
dir: ".harness",
|
|
1207
|
-
dbPath: ".harness/harness.db",
|
|
1208
1439
|
tasks: { adapter: params.tasksAdapter },
|
|
1209
1440
|
sections: {
|
|
1210
1441
|
toolsUsed: true,
|
|
@@ -1267,50 +1498,60 @@ async function runInit(cwd2, flags) {
|
|
|
1267
1498
|
if (flags.name) {
|
|
1268
1499
|
name = flags.name;
|
|
1269
1500
|
} else {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (p2.isCancel(descVal)) {
|
|
1286
|
-
p2.cancel("Cancelled.");
|
|
1287
|
-
process.exit(0);
|
|
1501
|
+
name = await cliFormWithRetry(
|
|
1502
|
+
async () => {
|
|
1503
|
+
const val = await p3.text({
|
|
1504
|
+
message: "Project name",
|
|
1505
|
+
placeholder: "my-app",
|
|
1506
|
+
...detectedName && { initialValue: detectedName }
|
|
1507
|
+
});
|
|
1508
|
+
if (p3.isCancel(val)) {
|
|
1509
|
+
p3.cancel("Cancelled.");
|
|
1510
|
+
process.exit(0);
|
|
1511
|
+
}
|
|
1512
|
+
return val;
|
|
1513
|
+
},
|
|
1514
|
+
initNameSchema
|
|
1515
|
+
);
|
|
1288
1516
|
}
|
|
1289
|
-
const description =
|
|
1517
|
+
const description = await cliFormWithRetry(
|
|
1518
|
+
async () => {
|
|
1519
|
+
const val = await p3.text({
|
|
1520
|
+
message: "Short description (shown to agents as context)",
|
|
1521
|
+
placeholder: "A REST API for managing notes"
|
|
1522
|
+
});
|
|
1523
|
+
if (p3.isCancel(val)) {
|
|
1524
|
+
p3.cancel("Cancelled.");
|
|
1525
|
+
process.exit(0);
|
|
1526
|
+
}
|
|
1527
|
+
return val;
|
|
1528
|
+
},
|
|
1529
|
+
initDescriptionSchema
|
|
1530
|
+
);
|
|
1290
1531
|
let provider;
|
|
1291
1532
|
if (flags.provider && ["claude-code", "opencode"].includes(flags.provider)) {
|
|
1292
1533
|
provider = flags.provider;
|
|
1293
1534
|
} else {
|
|
1294
|
-
const val = await
|
|
1535
|
+
const val = await p3.select({
|
|
1295
1536
|
message: "AI provider",
|
|
1296
1537
|
options: [
|
|
1297
1538
|
{ value: "claude-code", label: "Claude Code" },
|
|
1298
1539
|
{ value: "opencode", label: "OpenCode" }
|
|
1299
1540
|
]
|
|
1300
1541
|
});
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1542
|
+
if (p3.isCancel(val)) {
|
|
1543
|
+
p3.cancel("Cancelled.");
|
|
1303
1544
|
process.exit(0);
|
|
1304
1545
|
}
|
|
1305
1546
|
provider = val;
|
|
1306
1547
|
}
|
|
1307
1548
|
let globalInstallation = false;
|
|
1308
|
-
const globalVal = await
|
|
1549
|
+
const globalVal = await p3.confirm({
|
|
1309
1550
|
message: "Install globally (to home directory)?",
|
|
1310
1551
|
initialValue: false
|
|
1311
1552
|
});
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1553
|
+
if (p3.isCancel(globalVal)) {
|
|
1554
|
+
p3.cancel("Cancelled.");
|
|
1314
1555
|
process.exit(0);
|
|
1315
1556
|
}
|
|
1316
1557
|
if (globalVal) {
|
|
@@ -1320,21 +1561,26 @@ async function runInit(cwd2, flags) {
|
|
|
1320
1561
|
if (flags.docs) {
|
|
1321
1562
|
docsPath = flags.docs;
|
|
1322
1563
|
} else {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1564
|
+
docsPath = await cliFormWithRetry(
|
|
1565
|
+
async () => {
|
|
1566
|
+
const val = await p3.text({
|
|
1567
|
+
message: "Docs folder path (agents will search here)",
|
|
1568
|
+
initialValue: "./docs"
|
|
1569
|
+
});
|
|
1570
|
+
if (p3.isCancel(val)) {
|
|
1571
|
+
p3.cancel("Cancelled.");
|
|
1572
|
+
process.exit(0);
|
|
1573
|
+
}
|
|
1574
|
+
return val;
|
|
1575
|
+
},
|
|
1576
|
+
initDocsSchema
|
|
1577
|
+
);
|
|
1332
1578
|
}
|
|
1333
1579
|
let tasksAdapter;
|
|
1334
1580
|
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
1335
1581
|
tasksAdapter = flags.tasks;
|
|
1336
1582
|
} else {
|
|
1337
|
-
const val = await
|
|
1583
|
+
const val = await p3.select({
|
|
1338
1584
|
message: "Task adapter",
|
|
1339
1585
|
options: [
|
|
1340
1586
|
{ value: "local", label: "Local (feature_list.json)" },
|
|
@@ -1342,50 +1588,54 @@ async function runInit(cwd2, flags) {
|
|
|
1342
1588
|
{ value: "linear", label: "Linear (coming soon)" }
|
|
1343
1589
|
]
|
|
1344
1590
|
});
|
|
1345
|
-
if (
|
|
1346
|
-
|
|
1591
|
+
if (p3.isCancel(val)) {
|
|
1592
|
+
p3.cancel("Cancelled");
|
|
1347
1593
|
process.exit(0);
|
|
1348
1594
|
}
|
|
1349
1595
|
tasksAdapter = val;
|
|
1350
1596
|
}
|
|
1351
|
-
const addFirstTask = await
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1597
|
+
const addFirstTask = await p3.confirm({ message: "Add your first task now?", initialValue: true });
|
|
1598
|
+
if (p3.isCancel(addFirstTask)) {
|
|
1599
|
+
p3.cancel("Cancelled");
|
|
1354
1600
|
process.exit(0);
|
|
1355
1601
|
}
|
|
1356
1602
|
let firstTask;
|
|
1357
1603
|
if (addFirstTask) {
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1604
|
+
const taskTitle = await cliFormWithRetry(
|
|
1605
|
+
async () => {
|
|
1606
|
+
const val = await p3.text({ message: "Task title" });
|
|
1607
|
+
if (p3.isCancel(val)) {
|
|
1608
|
+
p3.cancel("Cancelled");
|
|
1609
|
+
process.exit(0);
|
|
1610
|
+
}
|
|
1611
|
+
return val.trim();
|
|
1612
|
+
},
|
|
1613
|
+
taskTitleSchema
|
|
1614
|
+
);
|
|
1615
|
+
const taskDesc = await cliFormWithRetry(
|
|
1616
|
+
async () => {
|
|
1617
|
+
const val = await p3.text({ message: "Task description", placeholder: "What and why" });
|
|
1618
|
+
if (p3.isCancel(val)) {
|
|
1619
|
+
p3.cancel("Cancelled");
|
|
1620
|
+
process.exit(0);
|
|
1621
|
+
}
|
|
1622
|
+
return val.trim();
|
|
1623
|
+
},
|
|
1624
|
+
taskDescriptionSchema
|
|
1625
|
+
);
|
|
1376
1626
|
const acceptance = [];
|
|
1377
|
-
|
|
1627
|
+
p3.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
1378
1628
|
while (true) {
|
|
1379
|
-
const criterionVal = await
|
|
1629
|
+
const criterionVal = await p3.text({
|
|
1380
1630
|
message: ">",
|
|
1381
1631
|
placeholder: "Criterion (or press Enter to finish)"
|
|
1382
1632
|
});
|
|
1383
|
-
if (
|
|
1633
|
+
if (p3.isCancel(criterionVal) || !criterionVal.trim()) break;
|
|
1384
1634
|
acceptance.push(criterionVal.trim());
|
|
1385
1635
|
}
|
|
1386
1636
|
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
1387
1637
|
}
|
|
1388
|
-
const spinner5 =
|
|
1638
|
+
const spinner5 = p3.spinner();
|
|
1389
1639
|
spinner5.start("Scaffolding...");
|
|
1390
1640
|
try {
|
|
1391
1641
|
const config = applyConfigDefaults({ name, description, provider, docsPath, tasksAdapter });
|
|
@@ -1408,23 +1658,23 @@ async function runInit(cwd2, flags) {
|
|
|
1408
1658
|
});
|
|
1409
1659
|
writeFileSync7(join10(installDir, "agent-harness-kit.config.ts"), configContent, "utf8");
|
|
1410
1660
|
mkdirSync6(join10(installDir, config.storage.dir), { recursive: true });
|
|
1411
|
-
const db = openDB(config, installDir);
|
|
1661
|
+
const db = await openDB(config, installDir);
|
|
1412
1662
|
await materializer.scaffold(config, { cwd: installDir, firstTask });
|
|
1413
1663
|
if (firstTask) {
|
|
1414
1664
|
const slug = slugify(firstTask.title);
|
|
1415
|
-
db.addTask({
|
|
1665
|
+
await db.addTask({
|
|
1416
1666
|
slug,
|
|
1417
1667
|
title: firstTask.title,
|
|
1418
1668
|
description: firstTask.description,
|
|
1419
1669
|
acceptance: firstTask.acceptance
|
|
1420
1670
|
});
|
|
1421
1671
|
}
|
|
1422
|
-
db.close();
|
|
1672
|
+
await db.close();
|
|
1423
1673
|
spinner5.stop("");
|
|
1424
1674
|
} catch (err) {
|
|
1425
1675
|
spinner5.stop("Failed");
|
|
1426
|
-
|
|
1427
|
-
|
|
1676
|
+
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1677
|
+
throw err;
|
|
1428
1678
|
}
|
|
1429
1679
|
const agentHarnessKitDir = globalInstallation ? "home directory" : "current directory";
|
|
1430
1680
|
console.log(pc6.green(`\u2713 Scaffolded harness in ${agentHarnessKitDir}`));
|
|
@@ -1448,7 +1698,7 @@ async function runInit(cwd2, flags) {
|
|
|
1448
1698
|
}
|
|
1449
1699
|
|
|
1450
1700
|
// src/commands/migrate.ts
|
|
1451
|
-
import * as
|
|
1701
|
+
import * as p4 from "@clack/prompts";
|
|
1452
1702
|
import pc7 from "picocolors";
|
|
1453
1703
|
async function runMigrate(cwd2, opts) {
|
|
1454
1704
|
const config = await loadConfig(cwd2);
|
|
@@ -1456,15 +1706,15 @@ async function runMigrate(cwd2, opts) {
|
|
|
1456
1706
|
if (opts.to && ["claude-code", "opencode"].includes(opts.to)) {
|
|
1457
1707
|
target = opts.to;
|
|
1458
1708
|
} else {
|
|
1459
|
-
const val = await
|
|
1709
|
+
const val = await p4.select({
|
|
1460
1710
|
message: "Migrate to provider",
|
|
1461
1711
|
options: [
|
|
1462
1712
|
{ value: "claude-code", label: "Claude Code" },
|
|
1463
1713
|
{ value: "opencode", label: "OpenCode" }
|
|
1464
1714
|
]
|
|
1465
1715
|
});
|
|
1466
|
-
if (
|
|
1467
|
-
|
|
1716
|
+
if (p4.isCancel(val)) {
|
|
1717
|
+
p4.cancel("Cancelled.");
|
|
1468
1718
|
process.exit(0);
|
|
1469
1719
|
}
|
|
1470
1720
|
target = val;
|
|
@@ -1473,24 +1723,159 @@ async function runMigrate(cwd2, opts) {
|
|
|
1473
1723
|
console.log(pc7.dim(`Already on ${target} \u2014 nothing to migrate.`));
|
|
1474
1724
|
return;
|
|
1475
1725
|
}
|
|
1476
|
-
const spinner5 =
|
|
1726
|
+
const spinner5 = p4.spinner();
|
|
1477
1727
|
spinner5.start(`Migrating from ${config.provider} to ${target}...`);
|
|
1478
1728
|
try {
|
|
1479
1729
|
const targetMaterializer = getMaterializer(target);
|
|
1480
1730
|
await targetMaterializer.build(config, cwd2);
|
|
1481
1731
|
spinner5.stop(pc7.green(`Migrated to ${target}`));
|
|
1482
|
-
|
|
1483
|
-
|
|
1732
|
+
p4.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
|
|
1733
|
+
p4.log.warn(`Then run: ahk build`);
|
|
1484
1734
|
} catch (err) {
|
|
1485
1735
|
spinner5.stop(pc7.red("Migration failed"));
|
|
1486
|
-
|
|
1736
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1487
1737
|
process.exit(1);
|
|
1488
1738
|
}
|
|
1489
1739
|
}
|
|
1490
1740
|
|
|
1491
|
-
// src/
|
|
1492
|
-
import {
|
|
1741
|
+
// src/commands/reset.ts
|
|
1742
|
+
import { existsSync as existsSync8, readdirSync, rmSync } from "fs";
|
|
1493
1743
|
import { join as join11, resolve as resolve7 } from "path";
|
|
1744
|
+
import * as p5 from "@clack/prompts";
|
|
1745
|
+
import pc8 from "picocolors";
|
|
1746
|
+
var AGENT_MD_FILES = ["lead", "explorer", "builder", "reviewer"];
|
|
1747
|
+
async function resetAgentMds(cwd2, provider) {
|
|
1748
|
+
const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
1749
|
+
const agentDirPath = resolve7(cwd2, agentDir);
|
|
1750
|
+
if (!existsSync8(agentDirPath)) {
|
|
1751
|
+
console.log(pc8.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const existingFiles = [];
|
|
1755
|
+
try {
|
|
1756
|
+
const files = readdirSync(agentDirPath);
|
|
1757
|
+
for (const f of files) {
|
|
1758
|
+
if (f.endsWith(".md") && AGENT_MD_FILES.includes(f.replace(".md", ""))) {
|
|
1759
|
+
existingFiles.push(f);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
} catch {
|
|
1763
|
+
console.log(pc8.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (existingFiles.length === 0) {
|
|
1767
|
+
console.log(pc8.yellow(` No agent MD files found in ${agentDir}/`));
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
for (const file of existingFiles) {
|
|
1771
|
+
const confirm3 = await p5.confirm({
|
|
1772
|
+
message: `Remove ${file}?`,
|
|
1773
|
+
initialValue: true
|
|
1774
|
+
});
|
|
1775
|
+
if (p5.isCancel(confirm3)) {
|
|
1776
|
+
console.log(pc8.red(" Cancelled by user."));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (confirm3) {
|
|
1780
|
+
try {
|
|
1781
|
+
const filePath = join11(agentDirPath, file);
|
|
1782
|
+
rmSync(filePath, { force: true });
|
|
1783
|
+
console.log(pc8.green(` Removed ${file}`));
|
|
1784
|
+
} catch {
|
|
1785
|
+
console.error(pc8.red(` Failed to remove ${file}`));
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
console.log(pc8.cyan(` Skipped ${file}`));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
async function runReset(cwd2, opts) {
|
|
1793
|
+
let config;
|
|
1794
|
+
try {
|
|
1795
|
+
config = await loadConfig(cwd2);
|
|
1796
|
+
} catch {
|
|
1797
|
+
console.error(pc8.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
const storageDir = config.storage.dir || ".harness";
|
|
1801
|
+
const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
|
|
1802
|
+
const featureListPath = resolve7(cwd2, storageDir, "feature_list.json");
|
|
1803
|
+
let resetDb = false;
|
|
1804
|
+
let resetFeatureList = false;
|
|
1805
|
+
let resetAgentMdsFlag = false;
|
|
1806
|
+
if (dbPath && existsSync8(dbPath)) {
|
|
1807
|
+
if (opts.force) {
|
|
1808
|
+
resetDb = true;
|
|
1809
|
+
} else {
|
|
1810
|
+
if (config.database.type !== "sqlite") {
|
|
1811
|
+
console.log(pc8.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
|
|
1812
|
+
resetDb = false;
|
|
1813
|
+
} else {
|
|
1814
|
+
const confirm3 = await p5.confirm({
|
|
1815
|
+
message: `Delete database (${config.database.path})?`,
|
|
1816
|
+
initialValue: true
|
|
1817
|
+
});
|
|
1818
|
+
if (p5.isCancel(confirm3)) {
|
|
1819
|
+
console.log(pc8.red(" Cancelled by user."));
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
resetDb = confirm3;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
} else if (!dbPath) {
|
|
1826
|
+
console.log(pc8.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
|
|
1827
|
+
}
|
|
1828
|
+
if (existsSync8(featureListPath)) {
|
|
1829
|
+
if (opts.force) {
|
|
1830
|
+
resetFeatureList = true;
|
|
1831
|
+
} else {
|
|
1832
|
+
const confirm3 = await p5.confirm({
|
|
1833
|
+
message: `Delete feature list (${storageDir}/feature_list.json)?`,
|
|
1834
|
+
initialValue: true
|
|
1835
|
+
});
|
|
1836
|
+
if (p5.isCancel(confirm3)) {
|
|
1837
|
+
console.log(pc8.red(" Cancelled by user."));
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
resetFeatureList = confirm3;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (opts.provider) {
|
|
1844
|
+
resetAgentMdsFlag = true;
|
|
1845
|
+
}
|
|
1846
|
+
if (resetDb && dbPath) {
|
|
1847
|
+
try {
|
|
1848
|
+
rmSync(dbPath, { force: true });
|
|
1849
|
+
rmSync(`${dbPath}-wal`, { force: true });
|
|
1850
|
+
rmSync(`${dbPath}-shm`, { force: true });
|
|
1851
|
+
console.log(pc8.green(` \u2713 Removed ${dbPath}`));
|
|
1852
|
+
} catch {
|
|
1853
|
+
console.error(pc8.red(` \u2717 Failed to remove ${dbPath}`));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (resetFeatureList) {
|
|
1857
|
+
try {
|
|
1858
|
+
rmSync(featureListPath, { force: true });
|
|
1859
|
+
console.log(pc8.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
1860
|
+
} catch {
|
|
1861
|
+
console.error(pc8.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (resetAgentMdsFlag) {
|
|
1865
|
+
console.log("");
|
|
1866
|
+
await resetAgentMds(cwd2, opts.provider || "claude-code");
|
|
1867
|
+
}
|
|
1868
|
+
if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
|
|
1869
|
+
console.log(pc8.yellow(" Nothing to reset (all items missing or skipped)."));
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
console.log("");
|
|
1873
|
+
console.log(pc8.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/core/mcp-server.ts
|
|
1877
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
1878
|
+
import { join as join12, resolve as resolve8 } from "path";
|
|
1494
1879
|
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
1495
1880
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1496
1881
|
import {
|
|
@@ -1634,6 +2019,24 @@ var TOOLS = [
|
|
|
1634
2019
|
required: ["criterionId"]
|
|
1635
2020
|
}
|
|
1636
2021
|
},
|
|
2022
|
+
{
|
|
2023
|
+
name: "tasks.add",
|
|
2024
|
+
description: "Create a new task in the harness. Use this when the user describes work in natural language. Infer slug, title, description, and acceptance criteria from the conversation. Ask for missing critical info before calling.",
|
|
2025
|
+
inputSchema: {
|
|
2026
|
+
type: "object",
|
|
2027
|
+
properties: {
|
|
2028
|
+
title: { type: "string", description: "Short human-readable title for the task" },
|
|
2029
|
+
slug: { type: "string", description: "URL-safe identifier (lowercase, hyphens). Auto-derived from title if omitted." },
|
|
2030
|
+
description: { type: "string", description: "Longer description of the task goal" },
|
|
2031
|
+
acceptance: {
|
|
2032
|
+
type: "array",
|
|
2033
|
+
items: { type: "string" },
|
|
2034
|
+
description: "List of acceptance criteria (plain sentences)"
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
required: ["title"]
|
|
2038
|
+
}
|
|
2039
|
+
},
|
|
1637
2040
|
{
|
|
1638
2041
|
name: "actions.record_tool",
|
|
1639
2042
|
description: "Record a tool call made during an action. This is the only way to populate the Tools dashboard. Call once per tool invocation.",
|
|
@@ -1650,8 +2053,8 @@ var TOOLS = [
|
|
|
1650
2053
|
}
|
|
1651
2054
|
];
|
|
1652
2055
|
async function startMcpServer(config, cwd2) {
|
|
1653
|
-
const db = openDB(config, cwd2);
|
|
1654
|
-
const docsPath =
|
|
2056
|
+
const db = await openDB(config, cwd2);
|
|
2057
|
+
const docsPath = resolve8(cwd2, config.project.docsPath);
|
|
1655
2058
|
const server = new Server(
|
|
1656
2059
|
{ name: "agent-harness-kit", version: VERSION },
|
|
1657
2060
|
{ capabilities: { tools: {} } }
|
|
@@ -1675,52 +2078,62 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1675
2078
|
case "actions.start": {
|
|
1676
2079
|
const taskId = num(args, "taskId");
|
|
1677
2080
|
const agent = str(args, "agent");
|
|
1678
|
-
const action = db.startAction(taskId, agent);
|
|
2081
|
+
const action = await db.startAction(taskId, agent);
|
|
1679
2082
|
return ok(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
|
|
1680
2083
|
}
|
|
1681
2084
|
case "actions.write": {
|
|
1682
2085
|
const actionId = str(args, "actionId");
|
|
1683
2086
|
const sectionType = str(args, "sectionType");
|
|
1684
2087
|
const content = str(args, "content");
|
|
1685
|
-
db.writeSection(actionId, sectionType, content);
|
|
2088
|
+
await db.writeSection(actionId, sectionType, content);
|
|
1686
2089
|
return ok(JSON.stringify({ actionId, sectionType, recorded: true }));
|
|
1687
2090
|
}
|
|
1688
2091
|
case "actions.complete": {
|
|
1689
2092
|
const actionId = str(args, "actionId");
|
|
1690
2093
|
const summary = str(args, "summary");
|
|
1691
|
-
const action = db.completeAction(actionId, summary);
|
|
2094
|
+
const action = await db.completeAction(actionId, summary);
|
|
1692
2095
|
return ok(JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at }));
|
|
1693
2096
|
}
|
|
1694
2097
|
case "actions.get": {
|
|
1695
2098
|
const taskId = num(args, "taskId");
|
|
1696
|
-
const actions = db.getActionsForTask(taskId);
|
|
1697
|
-
const full =
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2099
|
+
const actions = await db.getActionsForTask(taskId);
|
|
2100
|
+
const full = await Promise.all(
|
|
2101
|
+
actions.map(async (a) => ({
|
|
2102
|
+
...a,
|
|
2103
|
+
sections: await db.getActionSections(a.id)
|
|
2104
|
+
}))
|
|
2105
|
+
);
|
|
1701
2106
|
return ok(JSON.stringify(full, null, 2));
|
|
1702
2107
|
}
|
|
1703
2108
|
case "tasks.get": {
|
|
1704
2109
|
const status = args["status"];
|
|
1705
|
-
const tasks = status ? db.getTasks(status) : db.getTasks();
|
|
2110
|
+
const tasks = status ? await db.getTasks(status) : await db.getTasks();
|
|
1706
2111
|
return ok(JSON.stringify(tasks, null, 2));
|
|
1707
2112
|
}
|
|
1708
2113
|
case "tasks.claim": {
|
|
1709
2114
|
const id = num(args, "id");
|
|
1710
2115
|
const agent = str(args, "agent");
|
|
1711
|
-
const task2 = db.claimTask(id, agent);
|
|
2116
|
+
const task2 = await db.claimTask(id, agent);
|
|
1712
2117
|
if (!task2) {
|
|
1713
2118
|
return ok(JSON.stringify({ error: "task_already_claimed", taskId: id }));
|
|
1714
2119
|
}
|
|
1715
2120
|
return ok(JSON.stringify(task2));
|
|
1716
2121
|
}
|
|
2122
|
+
case "tasks.add": {
|
|
2123
|
+
const title = str(args, "title");
|
|
2124
|
+
const slug = args["slug"] ?? slugify(title);
|
|
2125
|
+
const description = args["description"];
|
|
2126
|
+
const acceptance = args["acceptance"];
|
|
2127
|
+
const task2 = await db.addTask({ slug, title, description, acceptance });
|
|
2128
|
+
return ok(JSON.stringify(task2));
|
|
2129
|
+
}
|
|
1717
2130
|
case "tasks.update": {
|
|
1718
2131
|
const id = num(args, "id");
|
|
1719
2132
|
const status = str(args, "status");
|
|
1720
2133
|
if (status === "done") {
|
|
1721
|
-
db.closeOrphanedActions(id);
|
|
2134
|
+
await db.closeOrphanedActions(id);
|
|
1722
2135
|
}
|
|
1723
|
-
const task2 = db.updateTaskStatus(id, status);
|
|
2136
|
+
const task2 = await db.updateTaskStatus(id, status);
|
|
1724
2137
|
return ok(JSON.stringify(task2));
|
|
1725
2138
|
}
|
|
1726
2139
|
case "docs.search": {
|
|
@@ -1733,12 +2146,12 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1733
2146
|
const filePath = str(args, "filePath");
|
|
1734
2147
|
const operation = str(args, "operation");
|
|
1735
2148
|
const notes = args["notes"];
|
|
1736
|
-
db.recordFile(actionId, filePath, operation, notes);
|
|
2149
|
+
await db.recordFile(actionId, filePath, operation, notes);
|
|
1737
2150
|
return ok(JSON.stringify({ actionId, filePath, operation, recorded: true }));
|
|
1738
2151
|
}
|
|
1739
2152
|
case "tasks.acceptance.update": {
|
|
1740
2153
|
const criterionId = num(args, "criterionId");
|
|
1741
|
-
db.markAcceptanceMet(criterionId);
|
|
2154
|
+
await db.markAcceptanceMet(criterionId);
|
|
1742
2155
|
return ok(JSON.stringify({ criterionId, met: true }));
|
|
1743
2156
|
}
|
|
1744
2157
|
case "actions.record_tool": {
|
|
@@ -1746,7 +2159,7 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1746
2159
|
const toolName = str(args, "toolName");
|
|
1747
2160
|
const argsJson = args["argsJson"];
|
|
1748
2161
|
const resultSummary = args["resultSummary"];
|
|
1749
|
-
db.recordTool(actionId, toolName, argsJson, resultSummary);
|
|
2162
|
+
await db.recordTool(actionId, toolName, argsJson, resultSummary);
|
|
1750
2163
|
return ok(JSON.stringify({ actionId, toolName, recorded: true }));
|
|
1751
2164
|
}
|
|
1752
2165
|
default:
|
|
@@ -1781,8 +2194,8 @@ function searchDocs(docsPath, query, maxResults = 10) {
|
|
|
1781
2194
|
function collectMarkdownFiles(dir) {
|
|
1782
2195
|
const files = [];
|
|
1783
2196
|
try {
|
|
1784
|
-
for (const entry of
|
|
1785
|
-
const full =
|
|
2197
|
+
for (const entry of readdirSync2(dir)) {
|
|
2198
|
+
const full = join12(dir, entry);
|
|
1786
2199
|
const stat = statSync(full);
|
|
1787
2200
|
if (stat.isDirectory()) {
|
|
1788
2201
|
files.push(...collectMarkdownFiles(full));
|
|
@@ -1798,14 +2211,14 @@ function ok(text3, isError = false) {
|
|
|
1798
2211
|
return { content: [{ type: "text", text: text3 }], isError };
|
|
1799
2212
|
}
|
|
1800
2213
|
function str(args, key) {
|
|
1801
|
-
const
|
|
1802
|
-
if (typeof
|
|
1803
|
-
return
|
|
2214
|
+
const v4 = args[key];
|
|
2215
|
+
if (typeof v4 !== "string") throw new Error(`${key} must be a string`);
|
|
2216
|
+
return v4;
|
|
1804
2217
|
}
|
|
1805
2218
|
function num(args, key) {
|
|
1806
|
-
const
|
|
1807
|
-
if (typeof
|
|
1808
|
-
return
|
|
2219
|
+
const v4 = args[key];
|
|
2220
|
+
if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
|
|
2221
|
+
return v4;
|
|
1809
2222
|
}
|
|
1810
2223
|
|
|
1811
2224
|
// src/commands/serve.ts
|
|
@@ -1821,34 +2234,36 @@ async function runServe(cwd2, opts) {
|
|
|
1821
2234
|
|
|
1822
2235
|
// src/commands/status.ts
|
|
1823
2236
|
import Table from "cli-table3";
|
|
1824
|
-
import
|
|
2237
|
+
import pc9 from "picocolors";
|
|
1825
2238
|
var STATUS_COLOR = {
|
|
1826
|
-
pending: (s) =>
|
|
1827
|
-
in_progress: (s) =>
|
|
1828
|
-
done: (s) =>
|
|
1829
|
-
blocked: (s) =>
|
|
2239
|
+
pending: (s) => pc9.dim(s),
|
|
2240
|
+
in_progress: (s) => pc9.cyan(s),
|
|
2241
|
+
done: (s) => pc9.green(s),
|
|
2242
|
+
blocked: (s) => pc9.red(s)
|
|
1830
2243
|
};
|
|
1831
2244
|
async function runStatus(cwd2, opts) {
|
|
1832
2245
|
const config = await loadConfig(cwd2);
|
|
1833
|
-
const db = openDB(config, cwd2);
|
|
2246
|
+
const db = await openDB(config, cwd2);
|
|
1834
2247
|
try {
|
|
1835
|
-
const tasks = db.getTasks();
|
|
1836
|
-
const summary = db.getStatusSummary();
|
|
2248
|
+
const tasks = await db.getTasks();
|
|
2249
|
+
const summary = await db.getStatusSummary();
|
|
1837
2250
|
if (opts.json) {
|
|
1838
|
-
const actions =
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2251
|
+
const actions = await Promise.all(
|
|
2252
|
+
tasks.map(async (t) => ({
|
|
2253
|
+
...t,
|
|
2254
|
+
actions: await db.getActionsForTask(t.id),
|
|
2255
|
+
acceptance: await db.getTaskAcceptance(t.id)
|
|
2256
|
+
}))
|
|
2257
|
+
);
|
|
1843
2258
|
console.log(JSON.stringify({ tasks: actions, summary }, null, 2));
|
|
1844
2259
|
return;
|
|
1845
2260
|
}
|
|
1846
2261
|
if (tasks.length === 0) {
|
|
1847
|
-
console.log(
|
|
2262
|
+
console.log(pc9.dim("No tasks yet. Run: ahk task add"));
|
|
1848
2263
|
return;
|
|
1849
2264
|
}
|
|
1850
2265
|
const table = new Table({
|
|
1851
|
-
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) =>
|
|
2266
|
+
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc9.bold(h)),
|
|
1852
2267
|
style: { head: [], border: [] }
|
|
1853
2268
|
});
|
|
1854
2269
|
for (const t of tasks) {
|
|
@@ -1866,12 +2281,12 @@ async function runStatus(cwd2, opts) {
|
|
|
1866
2281
|
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
1867
2282
|
if (inProgress.length > 0) {
|
|
1868
2283
|
console.log("");
|
|
1869
|
-
console.log(
|
|
2284
|
+
console.log(pc9.bold("Active actions:"));
|
|
1870
2285
|
for (const t of inProgress) {
|
|
1871
|
-
const actions = db.getActionsForTask(t.id);
|
|
2286
|
+
const actions = await db.getActionsForTask(t.id);
|
|
1872
2287
|
const active = actions.filter((a) => a.status === "in_progress");
|
|
1873
2288
|
for (const a of active) {
|
|
1874
|
-
console.log(` ${
|
|
2289
|
+
console.log(` ${pc9.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
1875
2290
|
}
|
|
1876
2291
|
}
|
|
1877
2292
|
}
|
|
@@ -1880,181 +2295,188 @@ async function runStatus(cwd2, opts) {
|
|
|
1880
2295
|
const fn = STATUS_COLOR[s.status] ?? ((x) => x);
|
|
1881
2296
|
return `${fn(s.status)}: ${s.total}`;
|
|
1882
2297
|
});
|
|
1883
|
-
console.log(
|
|
2298
|
+
console.log(pc9.dim("Tasks \u2014 ") + parts.join(pc9.dim(" | ")));
|
|
1884
2299
|
} finally {
|
|
1885
|
-
db.close();
|
|
2300
|
+
await db.close();
|
|
1886
2301
|
}
|
|
1887
2302
|
}
|
|
1888
2303
|
|
|
1889
2304
|
// src/commands/sync.ts
|
|
1890
|
-
import { existsSync as
|
|
1891
|
-
import { join as
|
|
1892
|
-
import
|
|
2305
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2306
|
+
import { join as join13, resolve as resolve9 } from "path";
|
|
2307
|
+
import pc10 from "picocolors";
|
|
1893
2308
|
async function runSync(cwd2, opts) {
|
|
1894
2309
|
const config = await loadConfig(cwd2);
|
|
1895
2310
|
const direction = opts.direction ?? "both";
|
|
1896
|
-
const featureListPath =
|
|
1897
|
-
const db = openDB(config, cwd2);
|
|
2311
|
+
const featureListPath = resolve9(join13(cwd2, config.storage.dir, "feature_list.json"));
|
|
2312
|
+
const db = await openDB(config, cwd2);
|
|
1898
2313
|
try {
|
|
1899
2314
|
if (direction === "in" || direction === "both") {
|
|
1900
2315
|
await syncIn(featureListPath, db, opts.dryRun ?? false);
|
|
1901
2316
|
}
|
|
1902
2317
|
if (direction === "out" || direction === "both") {
|
|
1903
|
-
syncOut(db, cwd2, opts.dryRun ?? false);
|
|
2318
|
+
await syncOut(db, cwd2, opts.dryRun ?? false);
|
|
1904
2319
|
}
|
|
1905
2320
|
} finally {
|
|
1906
|
-
db.close();
|
|
2321
|
+
await db.close();
|
|
1907
2322
|
}
|
|
1908
2323
|
}
|
|
1909
2324
|
async function syncIn(featureListPath, db, dryRun) {
|
|
1910
|
-
if (!
|
|
1911
|
-
console.log(
|
|
2325
|
+
if (!existsSync9(featureListPath)) {
|
|
2326
|
+
console.log(pc10.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
1912
2327
|
return;
|
|
1913
2328
|
}
|
|
1914
2329
|
let seeds;
|
|
1915
2330
|
try {
|
|
1916
2331
|
seeds = JSON.parse(readFileSync7(featureListPath, "utf8"));
|
|
1917
2332
|
} catch (err) {
|
|
1918
|
-
console.error(
|
|
2333
|
+
console.error(pc10.red(`Failed to parse feature_list.json: ${err}`));
|
|
1919
2334
|
process.exit(1);
|
|
1920
2335
|
}
|
|
1921
2336
|
if (dryRun) {
|
|
1922
|
-
console.log(
|
|
2337
|
+
console.log(pc10.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
1923
2338
|
for (const t of seeds) {
|
|
1924
|
-
const existing = db.getTaskBySlug(t.slug);
|
|
1925
|
-
console.log(` ${existing ?
|
|
2339
|
+
const existing = await db.getTaskBySlug(t.slug);
|
|
2340
|
+
console.log(` ${existing ? pc10.dim("skip") : pc10.green("add ")} ${t.slug}`);
|
|
1926
2341
|
}
|
|
1927
2342
|
return;
|
|
1928
2343
|
}
|
|
1929
|
-
const result = db.syncFromFeatureList(seeds);
|
|
1930
|
-
console.log(
|
|
2344
|
+
const result = await db.syncFromFeatureList(seeds);
|
|
2345
|
+
console.log(pc10.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
1931
2346
|
}
|
|
1932
|
-
function syncOut(db, cwd2, dryRun) {
|
|
2347
|
+
async function syncOut(db, cwd2, dryRun) {
|
|
1933
2348
|
if (dryRun) {
|
|
1934
|
-
const tasks = db.getTasks();
|
|
1935
|
-
console.log(
|
|
2349
|
+
const tasks = await db.getTasks();
|
|
2350
|
+
console.log(pc10.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
1936
2351
|
console.log(` ${tasks.length} tasks would be written`);
|
|
1937
2352
|
return;
|
|
1938
2353
|
}
|
|
1939
|
-
db.writeFeatureList(cwd2);
|
|
1940
|
-
console.log(
|
|
2354
|
+
await db.writeFeatureList(cwd2);
|
|
2355
|
+
console.log(pc10.green("\u2713 Out-sync: feature_list.json updated"));
|
|
1941
2356
|
}
|
|
1942
2357
|
|
|
1943
2358
|
// src/commands/task/add.ts
|
|
1944
|
-
import * as
|
|
1945
|
-
import
|
|
2359
|
+
import * as p6 from "@clack/prompts";
|
|
2360
|
+
import pc11 from "picocolors";
|
|
1946
2361
|
async function runTaskAdd(cwd2) {
|
|
1947
|
-
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2362
|
+
p6.intro(pc11.bold("agent-harness-kit \u2014 add task"));
|
|
2363
|
+
const title = await cliFormWithRetry(
|
|
2364
|
+
async () => {
|
|
2365
|
+
const val = await p6.text({ message: "Task title" });
|
|
2366
|
+
if (p6.isCancel(val)) {
|
|
2367
|
+
p6.cancel("Cancelled.");
|
|
2368
|
+
process.exit(0);
|
|
2369
|
+
}
|
|
2370
|
+
return val.trim();
|
|
2371
|
+
},
|
|
2372
|
+
taskTitleSchema
|
|
2373
|
+
);
|
|
2374
|
+
const description = await cliFormWithRetry(
|
|
2375
|
+
async () => {
|
|
2376
|
+
const val = await p6.text({
|
|
2377
|
+
message: "Description (what and why)",
|
|
2378
|
+
placeholder: "Describe the task in more detail, including any relevant context or instructions for the agents."
|
|
2379
|
+
});
|
|
2380
|
+
if (p6.isCancel(val)) {
|
|
2381
|
+
p6.cancel("Cancelled.");
|
|
2382
|
+
process.exit(0);
|
|
2383
|
+
}
|
|
2384
|
+
return val.trim();
|
|
2385
|
+
},
|
|
2386
|
+
taskDescriptionSchema
|
|
2387
|
+
);
|
|
1966
2388
|
const acceptance = [];
|
|
1967
|
-
|
|
2389
|
+
p6.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
1968
2390
|
while (true) {
|
|
1969
|
-
const val = await
|
|
1970
|
-
if (
|
|
2391
|
+
const val = await p6.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
|
|
2392
|
+
if (p6.isCancel(val) || !val || !val.trim()) break;
|
|
1971
2393
|
acceptance.push(val.trim());
|
|
1972
2394
|
}
|
|
1973
|
-
const spinner5 =
|
|
2395
|
+
const spinner5 = p6.spinner();
|
|
1974
2396
|
spinner5.start("Saving...");
|
|
1975
2397
|
try {
|
|
1976
2398
|
const config = await loadConfig(cwd2);
|
|
1977
|
-
const db = openDB(config, cwd2);
|
|
2399
|
+
const db = await openDB(config, cwd2);
|
|
1978
2400
|
const slug = slugify(title);
|
|
1979
|
-
const task2 = db.addTask({ slug, title, description: description || void 0, acceptance });
|
|
1980
|
-
db.writeFeatureList(cwd2);
|
|
1981
|
-
db.close();
|
|
2401
|
+
const task2 = await db.addTask({ slug, title, description: description || void 0, acceptance });
|
|
2402
|
+
await db.writeFeatureList(cwd2);
|
|
2403
|
+
await db.close();
|
|
1982
2404
|
spinner5.stop("");
|
|
1983
|
-
console.log(
|
|
1984
|
-
console.log(
|
|
2405
|
+
console.log(pc11.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
|
|
2406
|
+
console.log(pc11.cyan("\u2192") + " " + pc11.cyan("ahk status") + " to see all tasks");
|
|
1985
2407
|
} catch (err) {
|
|
1986
|
-
spinner5.stop(
|
|
1987
|
-
|
|
2408
|
+
spinner5.stop(pc11.red("Failed"));
|
|
2409
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1988
2410
|
process.exit(1);
|
|
1989
2411
|
}
|
|
1990
2412
|
}
|
|
1991
2413
|
|
|
1992
2414
|
// src/commands/task/done.ts
|
|
1993
2415
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1994
|
-
import { existsSync as
|
|
1995
|
-
import { resolve as
|
|
1996
|
-
import
|
|
2416
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2417
|
+
import { resolve as resolve10 } from "path";
|
|
2418
|
+
import pc12 from "picocolors";
|
|
1997
2419
|
async function runTaskDone(cwd2, idOrSlug) {
|
|
1998
2420
|
const config = await loadConfig(cwd2);
|
|
1999
2421
|
if (config.health.required) {
|
|
2000
|
-
const scriptPath =
|
|
2001
|
-
if (
|
|
2422
|
+
const scriptPath = resolve10(cwd2, config.health.scriptPath);
|
|
2423
|
+
if (existsSync10(scriptPath)) {
|
|
2002
2424
|
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
2003
2425
|
if (result.status !== 0) {
|
|
2004
|
-
console.error(
|
|
2426
|
+
console.error(pc12.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
2005
2427
|
if (result.stdout) console.error(result.stdout);
|
|
2006
2428
|
if (result.stderr) console.error(result.stderr);
|
|
2007
2429
|
process.exit(1);
|
|
2008
2430
|
}
|
|
2009
2431
|
}
|
|
2010
2432
|
}
|
|
2011
|
-
const db = openDB(config, cwd2);
|
|
2433
|
+
const db = await openDB(config, cwd2);
|
|
2012
2434
|
try {
|
|
2013
2435
|
const parsed = parseInt(idOrSlug, 10);
|
|
2014
2436
|
const isId = !isNaN(parsed);
|
|
2015
|
-
const task2 = isId ? db.getTaskById(parsed) : db.getTaskBySlug(idOrSlug);
|
|
2437
|
+
const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
|
|
2016
2438
|
if (!task2) {
|
|
2017
|
-
console.error(
|
|
2439
|
+
console.error(pc12.red(`Task not found: ${idOrSlug}`));
|
|
2018
2440
|
process.exit(1);
|
|
2019
2441
|
}
|
|
2020
2442
|
if (task2.status === "done") {
|
|
2021
|
-
console.log(
|
|
2443
|
+
console.log(pc12.dim(`Task #${task2.id} is already done.`));
|
|
2022
2444
|
return;
|
|
2023
2445
|
}
|
|
2024
|
-
db.updateTaskStatus(task2.id, "done");
|
|
2025
|
-
db.writeFeatureList(cwd2);
|
|
2026
|
-
console.log(
|
|
2446
|
+
await db.updateTaskStatus(task2.id, "done");
|
|
2447
|
+
await db.writeFeatureList(cwd2);
|
|
2448
|
+
console.log(pc12.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
2027
2449
|
} finally {
|
|
2028
|
-
db.close();
|
|
2450
|
+
await db.close();
|
|
2029
2451
|
}
|
|
2030
2452
|
}
|
|
2031
2453
|
|
|
2032
2454
|
// src/commands/task/list.ts
|
|
2033
2455
|
import Table2 from "cli-table3";
|
|
2034
|
-
import
|
|
2456
|
+
import pc13 from "picocolors";
|
|
2035
2457
|
var STATUS_COLOR2 = {
|
|
2036
|
-
pending: (s) =>
|
|
2037
|
-
in_progress: (s) =>
|
|
2038
|
-
done: (s) =>
|
|
2039
|
-
blocked: (s) =>
|
|
2458
|
+
pending: (s) => pc13.dim(s),
|
|
2459
|
+
in_progress: (s) => pc13.cyan(s),
|
|
2460
|
+
done: (s) => pc13.green(s),
|
|
2461
|
+
blocked: (s) => pc13.red(s)
|
|
2040
2462
|
};
|
|
2041
2463
|
async function runTaskList(cwd2, opts) {
|
|
2042
2464
|
const config = await loadConfig(cwd2);
|
|
2043
|
-
const db = openDB(config, cwd2);
|
|
2465
|
+
const db = await openDB(config, cwd2);
|
|
2044
2466
|
try {
|
|
2045
2467
|
const validStatuses = ["pending", "in_progress", "done", "blocked"];
|
|
2046
2468
|
const filterStatus = opts.status && validStatuses.includes(opts.status) ? opts.status : void 0;
|
|
2047
|
-
const tasks = filterStatus ? db.getTasks(filterStatus) : db.getTasks();
|
|
2469
|
+
const tasks = filterStatus ? await db.getTasks(filterStatus) : await db.getTasks();
|
|
2048
2470
|
if (opts.json) {
|
|
2049
2471
|
console.log(JSON.stringify(tasks, null, 2));
|
|
2050
2472
|
return;
|
|
2051
2473
|
}
|
|
2052
2474
|
if (tasks.length === 0) {
|
|
2053
|
-
console.log(
|
|
2475
|
+
console.log(pc13.dim("No tasks" + (filterStatus ? ` with status: ${filterStatus}` : "") + "."));
|
|
2054
2476
|
return;
|
|
2055
2477
|
}
|
|
2056
2478
|
const table = new Table2({
|
|
2057
|
-
head: ["ID", "Slug", "Title", "Status"].map((h) =>
|
|
2479
|
+
head: ["ID", "Slug", "Title", "Status"].map((h) => pc13.bold(h)),
|
|
2058
2480
|
style: { head: [], border: [] }
|
|
2059
2481
|
});
|
|
2060
2482
|
for (const t of tasks) {
|
|
@@ -2063,20 +2485,20 @@ async function runTaskList(cwd2, opts) {
|
|
|
2063
2485
|
}
|
|
2064
2486
|
console.log(table.toString());
|
|
2065
2487
|
} finally {
|
|
2066
|
-
db.close();
|
|
2488
|
+
await db.close();
|
|
2067
2489
|
}
|
|
2068
2490
|
}
|
|
2069
2491
|
|
|
2070
2492
|
// src/core/package-data.ts
|
|
2071
|
-
import { createRequire
|
|
2072
|
-
import { dirname as dirname5, join as
|
|
2493
|
+
import { createRequire } from "module";
|
|
2494
|
+
import { dirname as dirname5, join as join14 } from "path";
|
|
2073
2495
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2074
|
-
var require2 =
|
|
2075
|
-
var pkgPath =
|
|
2496
|
+
var require2 = createRequire(import.meta.url);
|
|
2497
|
+
var pkgPath = join14(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
2076
2498
|
var pkg = require2(pkgPath);
|
|
2077
2499
|
|
|
2078
2500
|
// src/core/update-check.ts
|
|
2079
|
-
import
|
|
2501
|
+
import pc14 from "picocolors";
|
|
2080
2502
|
var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
2081
2503
|
var TIMEOUT_MS = 2500;
|
|
2082
2504
|
function checkForUpdate(currentVersion) {
|
|
@@ -2094,22 +2516,22 @@ function checkForUpdate(currentVersion) {
|
|
|
2094
2516
|
}
|
|
2095
2517
|
function printUpdateMessage({ current, latest }) {
|
|
2096
2518
|
const lines = [
|
|
2097
|
-
` Update available ${
|
|
2098
|
-
` Run: ${
|
|
2519
|
+
` Update available ${pc14.dim(current)} \u2192 ${pc14.green(latest)} `,
|
|
2520
|
+
` Run: ${pc14.cyan(`npm i ${pkg.name}@${latest}`)} `
|
|
2099
2521
|
];
|
|
2100
2522
|
const width = Math.max(...lines.map((l) => stripAnsi2(l).length));
|
|
2101
2523
|
const border = "\u2500".repeat(width);
|
|
2102
2524
|
console.log();
|
|
2103
|
-
console.log(
|
|
2525
|
+
console.log(pc14.yellow(`\u250C${border}\u2510`));
|
|
2104
2526
|
for (const line of lines) {
|
|
2105
2527
|
const pad = width - stripAnsi2(line).length;
|
|
2106
|
-
console.log(
|
|
2528
|
+
console.log(pc14.yellow("\u2502") + line + " ".repeat(pad) + pc14.yellow("\u2502"));
|
|
2107
2529
|
}
|
|
2108
|
-
console.log(
|
|
2530
|
+
console.log(pc14.yellow(`\u2514${border}\u2518`));
|
|
2109
2531
|
console.log();
|
|
2110
2532
|
}
|
|
2111
2533
|
function isNewer(latest, current) {
|
|
2112
|
-
const toNum = (
|
|
2534
|
+
const toNum = (v4) => v4.split(".").map(Number);
|
|
2113
2535
|
const [lMaj, lMin, lPat] = toNum(latest);
|
|
2114
2536
|
const [cMaj, cMin, cPat] = toNum(current);
|
|
2115
2537
|
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
@@ -2120,134 +2542,6 @@ function stripAnsi2(str2) {
|
|
|
2120
2542
|
return str2.replace(/\x1B\[[0-9;]*m/g, "");
|
|
2121
2543
|
}
|
|
2122
2544
|
|
|
2123
|
-
// src/commands/reset.ts
|
|
2124
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, rmSync } from "fs";
|
|
2125
|
-
import { join as join14, resolve as resolve10 } from "path";
|
|
2126
|
-
import * as p5 from "@clack/prompts";
|
|
2127
|
-
import pc14 from "picocolors";
|
|
2128
|
-
async function resetAgentMds(cwd2, provider) {
|
|
2129
|
-
const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
2130
|
-
const agentDirPath = resolve10(cwd2, agentDir);
|
|
2131
|
-
if (!existsSync10(agentDirPath)) {
|
|
2132
|
-
console.log(pc14.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
|
|
2133
|
-
return;
|
|
2134
|
-
}
|
|
2135
|
-
const existingFiles = [];
|
|
2136
|
-
try {
|
|
2137
|
-
const files = readdirSync2(agentDirPath);
|
|
2138
|
-
for (const f of files) {
|
|
2139
|
-
if (f.endsWith(".md")) {
|
|
2140
|
-
existingFiles.push(f);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
} catch {
|
|
2144
|
-
console.log(pc14.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
if (existingFiles.length === 0) {
|
|
2148
|
-
console.log(pc14.yellow(` No agent MD files found in ${agentDir}/`));
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
for (const file of existingFiles) {
|
|
2152
|
-
const confirm3 = await p5.confirm({
|
|
2153
|
-
message: `Remove ${file}?`,
|
|
2154
|
-
initialValue: true
|
|
2155
|
-
});
|
|
2156
|
-
if (p5.isCancel(confirm3)) {
|
|
2157
|
-
console.log(pc14.red(" Cancelled by user."));
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
if (confirm3) {
|
|
2161
|
-
try {
|
|
2162
|
-
const filePath = join14(agentDirPath, file);
|
|
2163
|
-
rmSync(filePath, { force: true });
|
|
2164
|
-
console.log(pc14.green(` Removed ${file}`));
|
|
2165
|
-
} catch {
|
|
2166
|
-
console.error(pc14.red(` Failed to remove ${file}`));
|
|
2167
|
-
}
|
|
2168
|
-
} else {
|
|
2169
|
-
console.log(pc14.cyan(` Skipped ${file}`));
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
async function runReset(cwd2, opts) {
|
|
2174
|
-
let config;
|
|
2175
|
-
try {
|
|
2176
|
-
config = await loadConfig(cwd2);
|
|
2177
|
-
} catch {
|
|
2178
|
-
console.error(pc14.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
|
|
2179
|
-
process.exit(1);
|
|
2180
|
-
}
|
|
2181
|
-
const storageDir = config.storage.dir || ".harness";
|
|
2182
|
-
const dbPath = resolve10(cwd2, storageDir, "harness.db");
|
|
2183
|
-
const featureListPath = resolve10(cwd2, storageDir, "feature_list.json");
|
|
2184
|
-
let resetDb = false;
|
|
2185
|
-
let resetFeatureList = false;
|
|
2186
|
-
let resetAgentMdsFlag = false;
|
|
2187
|
-
if (existsSync10(dbPath)) {
|
|
2188
|
-
if (opts.force) {
|
|
2189
|
-
resetDb = true;
|
|
2190
|
-
} else {
|
|
2191
|
-
const confirm3 = await p5.confirm({
|
|
2192
|
-
message: `Delete database (${storageDir}/harness.db)?`,
|
|
2193
|
-
initialValue: true
|
|
2194
|
-
});
|
|
2195
|
-
if (p5.isCancel(confirm3)) {
|
|
2196
|
-
console.log(pc14.red(" Cancelled by user."));
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
resetDb = confirm3;
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
if (existsSync10(featureListPath)) {
|
|
2203
|
-
if (opts.force) {
|
|
2204
|
-
resetFeatureList = true;
|
|
2205
|
-
} else {
|
|
2206
|
-
const confirm3 = await p5.confirm({
|
|
2207
|
-
message: `Delete feature list (${storageDir}/feature_list.json)?`,
|
|
2208
|
-
initialValue: true
|
|
2209
|
-
});
|
|
2210
|
-
if (p5.isCancel(confirm3)) {
|
|
2211
|
-
console.log(pc14.red(" Cancelled by user."));
|
|
2212
|
-
return;
|
|
2213
|
-
}
|
|
2214
|
-
resetFeatureList = confirm3;
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
if (opts.provider) {
|
|
2218
|
-
resetAgentMdsFlag = true;
|
|
2219
|
-
}
|
|
2220
|
-
let changed = false;
|
|
2221
|
-
if (resetDb) {
|
|
2222
|
-
try {
|
|
2223
|
-
rmSync(dbPath, { force: true });
|
|
2224
|
-
console.log(pc14.green(` \u2713 Removed ${storageDir}/harness.db`));
|
|
2225
|
-
changed = true;
|
|
2226
|
-
} catch {
|
|
2227
|
-
console.error(pc14.red(` \u2717 Failed to remove ${dbPath}`));
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
if (resetFeatureList) {
|
|
2231
|
-
try {
|
|
2232
|
-
rmSync(featureListPath, { force: true });
|
|
2233
|
-
console.log(pc14.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
2234
|
-
changed = true;
|
|
2235
|
-
} catch {
|
|
2236
|
-
console.error(pc14.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
if (resetAgentMdsFlag) {
|
|
2240
|
-
console.log("");
|
|
2241
|
-
await resetAgentMds(cwd2, opts.provider || "claude-code");
|
|
2242
|
-
}
|
|
2243
|
-
if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
|
|
2244
|
-
console.log(pc14.yellow(" Nothing to reset (all items missing or skipped)."));
|
|
2245
|
-
return;
|
|
2246
|
-
}
|
|
2247
|
-
console.log("");
|
|
2248
|
-
console.log(pc14.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
2545
|
// src/cli.ts
|
|
2252
2546
|
var cwd = process.cwd();
|
|
2253
2547
|
var updateCheck = checkForUpdate(pkg.version);
|