@cardor/agent-harness-kit 0.18.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 +840 -547
- 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 +5 -1
- 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
|
-
).run(actionId, sectionType, content, now);
|
|
856
|
-
this.regenerateCurrentMd();
|
|
1040
|
+
await this.actions.create(id, taskId, agent, now);
|
|
1041
|
+
await this.regenerateCurrentMd();
|
|
1042
|
+
return await this.actions.getById(id);
|
|
857
1043
|
}
|
|
858
|
-
|
|
1044
|
+
async writeSection(actionId, sectionType, content) {
|
|
859
1045
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
-
this.
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
this.regenerateCurrentMd();
|
|
865
|
-
return this.getAction(actionId);
|
|
866
|
-
}
|
|
867
|
-
closeOrphanedActions(taskId) {
|
|
1046
|
+
await this.actions.addSection(actionId, sectionType, content, now);
|
|
1047
|
+
await this.regenerateCurrentMd();
|
|
1048
|
+
}
|
|
1049
|
+
async completeAction(actionId, summary) {
|
|
868
1050
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
getAction(actionId) {
|
|
876
|
-
return this.db.prepare(`SELECT * FROM actions WHERE id = ?`).get(actionId) ?? null;
|
|
877
|
-
}
|
|
878
|
-
getActionsForTask(taskId) {
|
|
879
|
-
return this.db.prepare(`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`).all(taskId);
|
|
880
|
-
}
|
|
881
|
-
getActionSections(actionId) {
|
|
882
|
-
return this.db.prepare(
|
|
883
|
-
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`
|
|
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) {
|
|
1051
|
+
await this.actions.complete(actionId, summary, now);
|
|
1052
|
+
await this.regenerateCurrentMd();
|
|
1053
|
+
return await this.actions.getById(actionId);
|
|
1054
|
+
}
|
|
1055
|
+
async closeOrphanedActions(taskId) {
|
|
893
1056
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
894
|
-
this.
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
return this.
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
getStatusSummary() {
|
|
918
|
-
return this.db.prepare(`SELECT status, COUNT(*) as total FROM tasks GROUP BY status`).all();
|
|
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) {
|
|
1072
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
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,9 +1347,59 @@ 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
|
|
|
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
|
+
|
|
1172
1403
|
// src/commands/init-helpers.ts
|
|
1173
1404
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1174
1405
|
import { join as join9 } from "path";
|
|
@@ -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,51 +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
|
-
});
|
|
1286
|
-
if (p2.isCancel(descVal)) {
|
|
1287
|
-
p2.cancel("Cancelled.");
|
|
1288
|
-
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
|
+
);
|
|
1289
1516
|
}
|
|
1290
|
-
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
|
+
);
|
|
1291
1531
|
let provider;
|
|
1292
1532
|
if (flags.provider && ["claude-code", "opencode"].includes(flags.provider)) {
|
|
1293
1533
|
provider = flags.provider;
|
|
1294
1534
|
} else {
|
|
1295
|
-
const val = await
|
|
1535
|
+
const val = await p3.select({
|
|
1296
1536
|
message: "AI provider",
|
|
1297
1537
|
options: [
|
|
1298
1538
|
{ value: "claude-code", label: "Claude Code" },
|
|
1299
1539
|
{ value: "opencode", label: "OpenCode" }
|
|
1300
1540
|
]
|
|
1301
1541
|
});
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1542
|
+
if (p3.isCancel(val)) {
|
|
1543
|
+
p3.cancel("Cancelled.");
|
|
1304
1544
|
process.exit(0);
|
|
1305
1545
|
}
|
|
1306
1546
|
provider = val;
|
|
1307
1547
|
}
|
|
1308
1548
|
let globalInstallation = false;
|
|
1309
|
-
const globalVal = await
|
|
1549
|
+
const globalVal = await p3.confirm({
|
|
1310
1550
|
message: "Install globally (to home directory)?",
|
|
1311
1551
|
initialValue: false
|
|
1312
1552
|
});
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
1553
|
+
if (p3.isCancel(globalVal)) {
|
|
1554
|
+
p3.cancel("Cancelled.");
|
|
1315
1555
|
process.exit(0);
|
|
1316
1556
|
}
|
|
1317
1557
|
if (globalVal) {
|
|
@@ -1321,21 +1561,26 @@ async function runInit(cwd2, flags) {
|
|
|
1321
1561
|
if (flags.docs) {
|
|
1322
1562
|
docsPath = flags.docs;
|
|
1323
1563
|
} else {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
+
);
|
|
1333
1578
|
}
|
|
1334
1579
|
let tasksAdapter;
|
|
1335
1580
|
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
1336
1581
|
tasksAdapter = flags.tasks;
|
|
1337
1582
|
} else {
|
|
1338
|
-
const val = await
|
|
1583
|
+
const val = await p3.select({
|
|
1339
1584
|
message: "Task adapter",
|
|
1340
1585
|
options: [
|
|
1341
1586
|
{ value: "local", label: "Local (feature_list.json)" },
|
|
@@ -1343,50 +1588,54 @@ async function runInit(cwd2, flags) {
|
|
|
1343
1588
|
{ value: "linear", label: "Linear (coming soon)" }
|
|
1344
1589
|
]
|
|
1345
1590
|
});
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1591
|
+
if (p3.isCancel(val)) {
|
|
1592
|
+
p3.cancel("Cancelled");
|
|
1348
1593
|
process.exit(0);
|
|
1349
1594
|
}
|
|
1350
1595
|
tasksAdapter = val;
|
|
1351
1596
|
}
|
|
1352
|
-
const addFirstTask = await
|
|
1353
|
-
if (
|
|
1354
|
-
|
|
1597
|
+
const addFirstTask = await p3.confirm({ message: "Add your first task now?", initialValue: true });
|
|
1598
|
+
if (p3.isCancel(addFirstTask)) {
|
|
1599
|
+
p3.cancel("Cancelled");
|
|
1355
1600
|
process.exit(0);
|
|
1356
1601
|
}
|
|
1357
1602
|
let firstTask;
|
|
1358
1603
|
if (addFirstTask) {
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
+
);
|
|
1377
1626
|
const acceptance = [];
|
|
1378
|
-
|
|
1627
|
+
p3.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
1379
1628
|
while (true) {
|
|
1380
|
-
const criterionVal = await
|
|
1629
|
+
const criterionVal = await p3.text({
|
|
1381
1630
|
message: ">",
|
|
1382
1631
|
placeholder: "Criterion (or press Enter to finish)"
|
|
1383
1632
|
});
|
|
1384
|
-
if (
|
|
1633
|
+
if (p3.isCancel(criterionVal) || !criterionVal.trim()) break;
|
|
1385
1634
|
acceptance.push(criterionVal.trim());
|
|
1386
1635
|
}
|
|
1387
1636
|
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
1388
1637
|
}
|
|
1389
|
-
const spinner5 =
|
|
1638
|
+
const spinner5 = p3.spinner();
|
|
1390
1639
|
spinner5.start("Scaffolding...");
|
|
1391
1640
|
try {
|
|
1392
1641
|
const config = applyConfigDefaults({ name, description, provider, docsPath, tasksAdapter });
|
|
@@ -1409,23 +1658,23 @@ async function runInit(cwd2, flags) {
|
|
|
1409
1658
|
});
|
|
1410
1659
|
writeFileSync7(join10(installDir, "agent-harness-kit.config.ts"), configContent, "utf8");
|
|
1411
1660
|
mkdirSync6(join10(installDir, config.storage.dir), { recursive: true });
|
|
1412
|
-
const db = openDB(config, installDir);
|
|
1661
|
+
const db = await openDB(config, installDir);
|
|
1413
1662
|
await materializer.scaffold(config, { cwd: installDir, firstTask });
|
|
1414
1663
|
if (firstTask) {
|
|
1415
1664
|
const slug = slugify(firstTask.title);
|
|
1416
|
-
db.addTask({
|
|
1665
|
+
await db.addTask({
|
|
1417
1666
|
slug,
|
|
1418
1667
|
title: firstTask.title,
|
|
1419
1668
|
description: firstTask.description,
|
|
1420
1669
|
acceptance: firstTask.acceptance
|
|
1421
1670
|
});
|
|
1422
1671
|
}
|
|
1423
|
-
db.close();
|
|
1672
|
+
await db.close();
|
|
1424
1673
|
spinner5.stop("");
|
|
1425
1674
|
} catch (err) {
|
|
1426
1675
|
spinner5.stop("Failed");
|
|
1427
|
-
|
|
1428
|
-
|
|
1676
|
+
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1677
|
+
throw err;
|
|
1429
1678
|
}
|
|
1430
1679
|
const agentHarnessKitDir = globalInstallation ? "home directory" : "current directory";
|
|
1431
1680
|
console.log(pc6.green(`\u2713 Scaffolded harness in ${agentHarnessKitDir}`));
|
|
@@ -1449,7 +1698,7 @@ async function runInit(cwd2, flags) {
|
|
|
1449
1698
|
}
|
|
1450
1699
|
|
|
1451
1700
|
// src/commands/migrate.ts
|
|
1452
|
-
import * as
|
|
1701
|
+
import * as p4 from "@clack/prompts";
|
|
1453
1702
|
import pc7 from "picocolors";
|
|
1454
1703
|
async function runMigrate(cwd2, opts) {
|
|
1455
1704
|
const config = await loadConfig(cwd2);
|
|
@@ -1457,15 +1706,15 @@ async function runMigrate(cwd2, opts) {
|
|
|
1457
1706
|
if (opts.to && ["claude-code", "opencode"].includes(opts.to)) {
|
|
1458
1707
|
target = opts.to;
|
|
1459
1708
|
} else {
|
|
1460
|
-
const val = await
|
|
1709
|
+
const val = await p4.select({
|
|
1461
1710
|
message: "Migrate to provider",
|
|
1462
1711
|
options: [
|
|
1463
1712
|
{ value: "claude-code", label: "Claude Code" },
|
|
1464
1713
|
{ value: "opencode", label: "OpenCode" }
|
|
1465
1714
|
]
|
|
1466
1715
|
});
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1716
|
+
if (p4.isCancel(val)) {
|
|
1717
|
+
p4.cancel("Cancelled.");
|
|
1469
1718
|
process.exit(0);
|
|
1470
1719
|
}
|
|
1471
1720
|
target = val;
|
|
@@ -1474,17 +1723,17 @@ async function runMigrate(cwd2, opts) {
|
|
|
1474
1723
|
console.log(pc7.dim(`Already on ${target} \u2014 nothing to migrate.`));
|
|
1475
1724
|
return;
|
|
1476
1725
|
}
|
|
1477
|
-
const spinner5 =
|
|
1726
|
+
const spinner5 = p4.spinner();
|
|
1478
1727
|
spinner5.start(`Migrating from ${config.provider} to ${target}...`);
|
|
1479
1728
|
try {
|
|
1480
1729
|
const targetMaterializer = getMaterializer(target);
|
|
1481
1730
|
await targetMaterializer.build(config, cwd2);
|
|
1482
1731
|
spinner5.stop(pc7.green(`Migrated to ${target}`));
|
|
1483
|
-
|
|
1484
|
-
|
|
1732
|
+
p4.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
|
|
1733
|
+
p4.log.warn(`Then run: ahk build`);
|
|
1485
1734
|
} catch (err) {
|
|
1486
1735
|
spinner5.stop(pc7.red("Migration failed"));
|
|
1487
|
-
|
|
1736
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1488
1737
|
process.exit(1);
|
|
1489
1738
|
}
|
|
1490
1739
|
}
|
|
@@ -1492,8 +1741,9 @@ async function runMigrate(cwd2, opts) {
|
|
|
1492
1741
|
// src/commands/reset.ts
|
|
1493
1742
|
import { existsSync as existsSync8, readdirSync, rmSync } from "fs";
|
|
1494
1743
|
import { join as join11, resolve as resolve7 } from "path";
|
|
1495
|
-
import * as
|
|
1744
|
+
import * as p5 from "@clack/prompts";
|
|
1496
1745
|
import pc8 from "picocolors";
|
|
1746
|
+
var AGENT_MD_FILES = ["lead", "explorer", "builder", "reviewer"];
|
|
1497
1747
|
async function resetAgentMds(cwd2, provider) {
|
|
1498
1748
|
const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
1499
1749
|
const agentDirPath = resolve7(cwd2, agentDir);
|
|
@@ -1505,7 +1755,7 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
1505
1755
|
try {
|
|
1506
1756
|
const files = readdirSync(agentDirPath);
|
|
1507
1757
|
for (const f of files) {
|
|
1508
|
-
if (f.endsWith(".md")) {
|
|
1758
|
+
if (f.endsWith(".md") && AGENT_MD_FILES.includes(f.replace(".md", ""))) {
|
|
1509
1759
|
existingFiles.push(f);
|
|
1510
1760
|
}
|
|
1511
1761
|
}
|
|
@@ -1518,11 +1768,11 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
1518
1768
|
return;
|
|
1519
1769
|
}
|
|
1520
1770
|
for (const file of existingFiles) {
|
|
1521
|
-
const confirm3 = await
|
|
1771
|
+
const confirm3 = await p5.confirm({
|
|
1522
1772
|
message: `Remove ${file}?`,
|
|
1523
1773
|
initialValue: true
|
|
1524
1774
|
});
|
|
1525
|
-
if (
|
|
1775
|
+
if (p5.isCancel(confirm3)) {
|
|
1526
1776
|
console.log(pc8.red(" Cancelled by user."));
|
|
1527
1777
|
return;
|
|
1528
1778
|
}
|
|
@@ -1548,35 +1798,42 @@ async function runReset(cwd2, opts) {
|
|
|
1548
1798
|
process.exit(1);
|
|
1549
1799
|
}
|
|
1550
1800
|
const storageDir = config.storage.dir || ".harness";
|
|
1551
|
-
const dbPath = resolve7(cwd2,
|
|
1801
|
+
const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
|
|
1552
1802
|
const featureListPath = resolve7(cwd2, storageDir, "feature_list.json");
|
|
1553
1803
|
let resetDb = false;
|
|
1554
1804
|
let resetFeatureList = false;
|
|
1555
1805
|
let resetAgentMdsFlag = false;
|
|
1556
|
-
if (existsSync8(dbPath)) {
|
|
1806
|
+
if (dbPath && existsSync8(dbPath)) {
|
|
1557
1807
|
if (opts.force) {
|
|
1558
1808
|
resetDb = true;
|
|
1559
1809
|
} else {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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;
|
|
1567
1823
|
}
|
|
1568
|
-
resetDb = confirm3;
|
|
1569
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.`));
|
|
1570
1827
|
}
|
|
1571
1828
|
if (existsSync8(featureListPath)) {
|
|
1572
1829
|
if (opts.force) {
|
|
1573
1830
|
resetFeatureList = true;
|
|
1574
1831
|
} else {
|
|
1575
|
-
const confirm3 = await
|
|
1832
|
+
const confirm3 = await p5.confirm({
|
|
1576
1833
|
message: `Delete feature list (${storageDir}/feature_list.json)?`,
|
|
1577
1834
|
initialValue: true
|
|
1578
1835
|
});
|
|
1579
|
-
if (
|
|
1836
|
+
if (p5.isCancel(confirm3)) {
|
|
1580
1837
|
console.log(pc8.red(" Cancelled by user."));
|
|
1581
1838
|
return;
|
|
1582
1839
|
}
|
|
@@ -1586,12 +1843,12 @@ async function runReset(cwd2, opts) {
|
|
|
1586
1843
|
if (opts.provider) {
|
|
1587
1844
|
resetAgentMdsFlag = true;
|
|
1588
1845
|
}
|
|
1589
|
-
|
|
1590
|
-
if (resetDb) {
|
|
1846
|
+
if (resetDb && dbPath) {
|
|
1591
1847
|
try {
|
|
1592
1848
|
rmSync(dbPath, { force: true });
|
|
1593
|
-
|
|
1594
|
-
|
|
1849
|
+
rmSync(`${dbPath}-wal`, { force: true });
|
|
1850
|
+
rmSync(`${dbPath}-shm`, { force: true });
|
|
1851
|
+
console.log(pc8.green(` \u2713 Removed ${dbPath}`));
|
|
1595
1852
|
} catch {
|
|
1596
1853
|
console.error(pc8.red(` \u2717 Failed to remove ${dbPath}`));
|
|
1597
1854
|
}
|
|
@@ -1600,7 +1857,6 @@ async function runReset(cwd2, opts) {
|
|
|
1600
1857
|
try {
|
|
1601
1858
|
rmSync(featureListPath, { force: true });
|
|
1602
1859
|
console.log(pc8.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
1603
|
-
changed = true;
|
|
1604
1860
|
} catch {
|
|
1605
1861
|
console.error(pc8.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
1606
1862
|
}
|
|
@@ -1763,6 +2019,24 @@ var TOOLS = [
|
|
|
1763
2019
|
required: ["criterionId"]
|
|
1764
2020
|
}
|
|
1765
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
|
+
},
|
|
1766
2040
|
{
|
|
1767
2041
|
name: "actions.record_tool",
|
|
1768
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.",
|
|
@@ -1779,7 +2053,7 @@ var TOOLS = [
|
|
|
1779
2053
|
}
|
|
1780
2054
|
];
|
|
1781
2055
|
async function startMcpServer(config, cwd2) {
|
|
1782
|
-
const db = openDB(config, cwd2);
|
|
2056
|
+
const db = await openDB(config, cwd2);
|
|
1783
2057
|
const docsPath = resolve8(cwd2, config.project.docsPath);
|
|
1784
2058
|
const server = new Server(
|
|
1785
2059
|
{ name: "agent-harness-kit", version: VERSION },
|
|
@@ -1804,52 +2078,62 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1804
2078
|
case "actions.start": {
|
|
1805
2079
|
const taskId = num(args, "taskId");
|
|
1806
2080
|
const agent = str(args, "agent");
|
|
1807
|
-
const action = db.startAction(taskId, agent);
|
|
2081
|
+
const action = await db.startAction(taskId, agent);
|
|
1808
2082
|
return ok(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
|
|
1809
2083
|
}
|
|
1810
2084
|
case "actions.write": {
|
|
1811
2085
|
const actionId = str(args, "actionId");
|
|
1812
2086
|
const sectionType = str(args, "sectionType");
|
|
1813
2087
|
const content = str(args, "content");
|
|
1814
|
-
db.writeSection(actionId, sectionType, content);
|
|
2088
|
+
await db.writeSection(actionId, sectionType, content);
|
|
1815
2089
|
return ok(JSON.stringify({ actionId, sectionType, recorded: true }));
|
|
1816
2090
|
}
|
|
1817
2091
|
case "actions.complete": {
|
|
1818
2092
|
const actionId = str(args, "actionId");
|
|
1819
2093
|
const summary = str(args, "summary");
|
|
1820
|
-
const action = db.completeAction(actionId, summary);
|
|
2094
|
+
const action = await db.completeAction(actionId, summary);
|
|
1821
2095
|
return ok(JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at }));
|
|
1822
2096
|
}
|
|
1823
2097
|
case "actions.get": {
|
|
1824
2098
|
const taskId = num(args, "taskId");
|
|
1825
|
-
const actions = db.getActionsForTask(taskId);
|
|
1826
|
-
const full =
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
+
);
|
|
1830
2106
|
return ok(JSON.stringify(full, null, 2));
|
|
1831
2107
|
}
|
|
1832
2108
|
case "tasks.get": {
|
|
1833
2109
|
const status = args["status"];
|
|
1834
|
-
const tasks = status ? db.getTasks(status) : db.getTasks();
|
|
2110
|
+
const tasks = status ? await db.getTasks(status) : await db.getTasks();
|
|
1835
2111
|
return ok(JSON.stringify(tasks, null, 2));
|
|
1836
2112
|
}
|
|
1837
2113
|
case "tasks.claim": {
|
|
1838
2114
|
const id = num(args, "id");
|
|
1839
2115
|
const agent = str(args, "agent");
|
|
1840
|
-
const task2 = db.claimTask(id, agent);
|
|
2116
|
+
const task2 = await db.claimTask(id, agent);
|
|
1841
2117
|
if (!task2) {
|
|
1842
2118
|
return ok(JSON.stringify({ error: "task_already_claimed", taskId: id }));
|
|
1843
2119
|
}
|
|
1844
2120
|
return ok(JSON.stringify(task2));
|
|
1845
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
|
+
}
|
|
1846
2130
|
case "tasks.update": {
|
|
1847
2131
|
const id = num(args, "id");
|
|
1848
2132
|
const status = str(args, "status");
|
|
1849
2133
|
if (status === "done") {
|
|
1850
|
-
db.closeOrphanedActions(id);
|
|
2134
|
+
await db.closeOrphanedActions(id);
|
|
1851
2135
|
}
|
|
1852
|
-
const task2 = db.updateTaskStatus(id, status);
|
|
2136
|
+
const task2 = await db.updateTaskStatus(id, status);
|
|
1853
2137
|
return ok(JSON.stringify(task2));
|
|
1854
2138
|
}
|
|
1855
2139
|
case "docs.search": {
|
|
@@ -1862,12 +2146,12 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1862
2146
|
const filePath = str(args, "filePath");
|
|
1863
2147
|
const operation = str(args, "operation");
|
|
1864
2148
|
const notes = args["notes"];
|
|
1865
|
-
db.recordFile(actionId, filePath, operation, notes);
|
|
2149
|
+
await db.recordFile(actionId, filePath, operation, notes);
|
|
1866
2150
|
return ok(JSON.stringify({ actionId, filePath, operation, recorded: true }));
|
|
1867
2151
|
}
|
|
1868
2152
|
case "tasks.acceptance.update": {
|
|
1869
2153
|
const criterionId = num(args, "criterionId");
|
|
1870
|
-
db.markAcceptanceMet(criterionId);
|
|
2154
|
+
await db.markAcceptanceMet(criterionId);
|
|
1871
2155
|
return ok(JSON.stringify({ criterionId, met: true }));
|
|
1872
2156
|
}
|
|
1873
2157
|
case "actions.record_tool": {
|
|
@@ -1875,7 +2159,7 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
1875
2159
|
const toolName = str(args, "toolName");
|
|
1876
2160
|
const argsJson = args["argsJson"];
|
|
1877
2161
|
const resultSummary = args["resultSummary"];
|
|
1878
|
-
db.recordTool(actionId, toolName, argsJson, resultSummary);
|
|
2162
|
+
await db.recordTool(actionId, toolName, argsJson, resultSummary);
|
|
1879
2163
|
return ok(JSON.stringify({ actionId, toolName, recorded: true }));
|
|
1880
2164
|
}
|
|
1881
2165
|
default:
|
|
@@ -1927,14 +2211,14 @@ function ok(text3, isError = false) {
|
|
|
1927
2211
|
return { content: [{ type: "text", text: text3 }], isError };
|
|
1928
2212
|
}
|
|
1929
2213
|
function str(args, key) {
|
|
1930
|
-
const
|
|
1931
|
-
if (typeof
|
|
1932
|
-
return
|
|
2214
|
+
const v4 = args[key];
|
|
2215
|
+
if (typeof v4 !== "string") throw new Error(`${key} must be a string`);
|
|
2216
|
+
return v4;
|
|
1933
2217
|
}
|
|
1934
2218
|
function num(args, key) {
|
|
1935
|
-
const
|
|
1936
|
-
if (typeof
|
|
1937
|
-
return
|
|
2219
|
+
const v4 = args[key];
|
|
2220
|
+
if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
|
|
2221
|
+
return v4;
|
|
1938
2222
|
}
|
|
1939
2223
|
|
|
1940
2224
|
// src/commands/serve.ts
|
|
@@ -1959,16 +2243,18 @@ var STATUS_COLOR = {
|
|
|
1959
2243
|
};
|
|
1960
2244
|
async function runStatus(cwd2, opts) {
|
|
1961
2245
|
const config = await loadConfig(cwd2);
|
|
1962
|
-
const db = openDB(config, cwd2);
|
|
2246
|
+
const db = await openDB(config, cwd2);
|
|
1963
2247
|
try {
|
|
1964
|
-
const tasks = db.getTasks();
|
|
1965
|
-
const summary = db.getStatusSummary();
|
|
2248
|
+
const tasks = await db.getTasks();
|
|
2249
|
+
const summary = await db.getStatusSummary();
|
|
1966
2250
|
if (opts.json) {
|
|
1967
|
-
const actions =
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
+
);
|
|
1972
2258
|
console.log(JSON.stringify({ tasks: actions, summary }, null, 2));
|
|
1973
2259
|
return;
|
|
1974
2260
|
}
|
|
@@ -1997,7 +2283,7 @@ async function runStatus(cwd2, opts) {
|
|
|
1997
2283
|
console.log("");
|
|
1998
2284
|
console.log(pc9.bold("Active actions:"));
|
|
1999
2285
|
for (const t of inProgress) {
|
|
2000
|
-
const actions = db.getActionsForTask(t.id);
|
|
2286
|
+
const actions = await db.getActionsForTask(t.id);
|
|
2001
2287
|
const active = actions.filter((a) => a.status === "in_progress");
|
|
2002
2288
|
for (const a of active) {
|
|
2003
2289
|
console.log(` ${pc9.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
@@ -2011,7 +2297,7 @@ async function runStatus(cwd2, opts) {
|
|
|
2011
2297
|
});
|
|
2012
2298
|
console.log(pc9.dim("Tasks \u2014 ") + parts.join(pc9.dim(" | ")));
|
|
2013
2299
|
} finally {
|
|
2014
|
-
db.close();
|
|
2300
|
+
await db.close();
|
|
2015
2301
|
}
|
|
2016
2302
|
}
|
|
2017
2303
|
|
|
@@ -2023,16 +2309,16 @@ async function runSync(cwd2, opts) {
|
|
|
2023
2309
|
const config = await loadConfig(cwd2);
|
|
2024
2310
|
const direction = opts.direction ?? "both";
|
|
2025
2311
|
const featureListPath = resolve9(join13(cwd2, config.storage.dir, "feature_list.json"));
|
|
2026
|
-
const db = openDB(config, cwd2);
|
|
2312
|
+
const db = await openDB(config, cwd2);
|
|
2027
2313
|
try {
|
|
2028
2314
|
if (direction === "in" || direction === "both") {
|
|
2029
2315
|
await syncIn(featureListPath, db, opts.dryRun ?? false);
|
|
2030
2316
|
}
|
|
2031
2317
|
if (direction === "out" || direction === "both") {
|
|
2032
|
-
syncOut(db, cwd2, opts.dryRun ?? false);
|
|
2318
|
+
await syncOut(db, cwd2, opts.dryRun ?? false);
|
|
2033
2319
|
}
|
|
2034
2320
|
} finally {
|
|
2035
|
-
db.close();
|
|
2321
|
+
await db.close();
|
|
2036
2322
|
}
|
|
2037
2323
|
}
|
|
2038
2324
|
async function syncIn(featureListPath, db, dryRun) {
|
|
@@ -2050,70 +2336,77 @@ async function syncIn(featureListPath, db, dryRun) {
|
|
|
2050
2336
|
if (dryRun) {
|
|
2051
2337
|
console.log(pc10.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
2052
2338
|
for (const t of seeds) {
|
|
2053
|
-
const existing = db.getTaskBySlug(t.slug);
|
|
2339
|
+
const existing = await db.getTaskBySlug(t.slug);
|
|
2054
2340
|
console.log(` ${existing ? pc10.dim("skip") : pc10.green("add ")} ${t.slug}`);
|
|
2055
2341
|
}
|
|
2056
2342
|
return;
|
|
2057
2343
|
}
|
|
2058
|
-
const result = db.syncFromFeatureList(seeds);
|
|
2344
|
+
const result = await db.syncFromFeatureList(seeds);
|
|
2059
2345
|
console.log(pc10.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
2060
2346
|
}
|
|
2061
|
-
function syncOut(db, cwd2, dryRun) {
|
|
2347
|
+
async function syncOut(db, cwd2, dryRun) {
|
|
2062
2348
|
if (dryRun) {
|
|
2063
|
-
const tasks = db.getTasks();
|
|
2349
|
+
const tasks = await db.getTasks();
|
|
2064
2350
|
console.log(pc10.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
2065
2351
|
console.log(` ${tasks.length} tasks would be written`);
|
|
2066
2352
|
return;
|
|
2067
2353
|
}
|
|
2068
|
-
db.writeFeatureList(cwd2);
|
|
2354
|
+
await db.writeFeatureList(cwd2);
|
|
2069
2355
|
console.log(pc10.green("\u2713 Out-sync: feature_list.json updated"));
|
|
2070
2356
|
}
|
|
2071
2357
|
|
|
2072
2358
|
// src/commands/task/add.ts
|
|
2073
|
-
import * as
|
|
2359
|
+
import * as p6 from "@clack/prompts";
|
|
2074
2360
|
import pc11 from "picocolors";
|
|
2075
2361
|
async function runTaskAdd(cwd2) {
|
|
2076
|
-
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
+
);
|
|
2095
2388
|
const acceptance = [];
|
|
2096
|
-
|
|
2389
|
+
p6.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
2097
2390
|
while (true) {
|
|
2098
|
-
const val = await
|
|
2099
|
-
if (
|
|
2391
|
+
const val = await p6.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
|
|
2392
|
+
if (p6.isCancel(val) || !val || !val.trim()) break;
|
|
2100
2393
|
acceptance.push(val.trim());
|
|
2101
2394
|
}
|
|
2102
|
-
const spinner5 =
|
|
2395
|
+
const spinner5 = p6.spinner();
|
|
2103
2396
|
spinner5.start("Saving...");
|
|
2104
2397
|
try {
|
|
2105
2398
|
const config = await loadConfig(cwd2);
|
|
2106
|
-
const db = openDB(config, cwd2);
|
|
2399
|
+
const db = await openDB(config, cwd2);
|
|
2107
2400
|
const slug = slugify(title);
|
|
2108
|
-
const task2 = db.addTask({ slug, title, description: description || void 0, acceptance });
|
|
2109
|
-
db.writeFeatureList(cwd2);
|
|
2110
|
-
db.close();
|
|
2401
|
+
const task2 = await db.addTask({ slug, title, description: description || void 0, acceptance });
|
|
2402
|
+
await db.writeFeatureList(cwd2);
|
|
2403
|
+
await db.close();
|
|
2111
2404
|
spinner5.stop("");
|
|
2112
2405
|
console.log(pc11.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
|
|
2113
2406
|
console.log(pc11.cyan("\u2192") + " " + pc11.cyan("ahk status") + " to see all tasks");
|
|
2114
2407
|
} catch (err) {
|
|
2115
2408
|
spinner5.stop(pc11.red("Failed"));
|
|
2116
|
-
|
|
2409
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
2117
2410
|
process.exit(1);
|
|
2118
2411
|
}
|
|
2119
2412
|
}
|
|
@@ -2137,11 +2430,11 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
2137
2430
|
}
|
|
2138
2431
|
}
|
|
2139
2432
|
}
|
|
2140
|
-
const db = openDB(config, cwd2);
|
|
2433
|
+
const db = await openDB(config, cwd2);
|
|
2141
2434
|
try {
|
|
2142
2435
|
const parsed = parseInt(idOrSlug, 10);
|
|
2143
2436
|
const isId = !isNaN(parsed);
|
|
2144
|
-
const task2 = isId ? db.getTaskById(parsed) : db.getTaskBySlug(idOrSlug);
|
|
2437
|
+
const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
|
|
2145
2438
|
if (!task2) {
|
|
2146
2439
|
console.error(pc12.red(`Task not found: ${idOrSlug}`));
|
|
2147
2440
|
process.exit(1);
|
|
@@ -2150,11 +2443,11 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
2150
2443
|
console.log(pc12.dim(`Task #${task2.id} is already done.`));
|
|
2151
2444
|
return;
|
|
2152
2445
|
}
|
|
2153
|
-
db.updateTaskStatus(task2.id, "done");
|
|
2154
|
-
db.writeFeatureList(cwd2);
|
|
2446
|
+
await db.updateTaskStatus(task2.id, "done");
|
|
2447
|
+
await db.writeFeatureList(cwd2);
|
|
2155
2448
|
console.log(pc12.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
2156
2449
|
} finally {
|
|
2157
|
-
db.close();
|
|
2450
|
+
await db.close();
|
|
2158
2451
|
}
|
|
2159
2452
|
}
|
|
2160
2453
|
|
|
@@ -2169,11 +2462,11 @@ var STATUS_COLOR2 = {
|
|
|
2169
2462
|
};
|
|
2170
2463
|
async function runTaskList(cwd2, opts) {
|
|
2171
2464
|
const config = await loadConfig(cwd2);
|
|
2172
|
-
const db = openDB(config, cwd2);
|
|
2465
|
+
const db = await openDB(config, cwd2);
|
|
2173
2466
|
try {
|
|
2174
2467
|
const validStatuses = ["pending", "in_progress", "done", "blocked"];
|
|
2175
2468
|
const filterStatus = opts.status && validStatuses.includes(opts.status) ? opts.status : void 0;
|
|
2176
|
-
const tasks = filterStatus ? db.getTasks(filterStatus) : db.getTasks();
|
|
2469
|
+
const tasks = filterStatus ? await db.getTasks(filterStatus) : await db.getTasks();
|
|
2177
2470
|
if (opts.json) {
|
|
2178
2471
|
console.log(JSON.stringify(tasks, null, 2));
|
|
2179
2472
|
return;
|
|
@@ -2192,15 +2485,15 @@ async function runTaskList(cwd2, opts) {
|
|
|
2192
2485
|
}
|
|
2193
2486
|
console.log(table.toString());
|
|
2194
2487
|
} finally {
|
|
2195
|
-
db.close();
|
|
2488
|
+
await db.close();
|
|
2196
2489
|
}
|
|
2197
2490
|
}
|
|
2198
2491
|
|
|
2199
2492
|
// src/core/package-data.ts
|
|
2200
|
-
import { createRequire
|
|
2493
|
+
import { createRequire } from "module";
|
|
2201
2494
|
import { dirname as dirname5, join as join14 } from "path";
|
|
2202
2495
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2203
|
-
var require2 =
|
|
2496
|
+
var require2 = createRequire(import.meta.url);
|
|
2204
2497
|
var pkgPath = join14(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
2205
2498
|
var pkg = require2(pkgPath);
|
|
2206
2499
|
|
|
@@ -2238,7 +2531,7 @@ function printUpdateMessage({ current, latest }) {
|
|
|
2238
2531
|
console.log();
|
|
2239
2532
|
}
|
|
2240
2533
|
function isNewer(latest, current) {
|
|
2241
|
-
const toNum = (
|
|
2534
|
+
const toNum = (v4) => v4.split(".").map(Number);
|
|
2242
2535
|
const [lMaj, lMin, lPat] = toNum(latest);
|
|
2243
2536
|
const [cMaj, cMin, cPat] = toNum(current);
|
|
2244
2537
|
if (lMaj !== cMaj) return lMaj > cMaj;
|