@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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-LQ7SDMK6.js";
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 = db.getStatusSummary();
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 [{ total: totalActions }] = db.queryRaw(`SELECT COUNT(*) as total FROM actions`);
498
- const [{ total: totalFiles }] = db.queryRaw(`SELECT COUNT(*) as total FROM action_files`);
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
- const rows = db.queryRaw(`
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 = db.getTaskById(id);
599
+ const task2 = await tasks.getById(id);
523
600
  if (!task2) return c.json({ error: "Not found" }, 404);
524
- const acceptance = db.getTaskAcceptance(id);
525
- const actions = db.getActionsForTask(id).map((action) => ({
526
- ...action,
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(db.getTopTools(limit));
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
- const rows = db.queryRaw(`
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
- const rows = db.queryRaw(`
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
- const rows = db.queryRaw(`
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
- const rows = db.queryRaw(`
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
- const rows = db.queryRaw(`
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
- const walPath = `${dbPath}-wal`;
650
- const watchTarget = existsSync5(walPath) ? walPath : dbPath;
651
- const watcher = watch2(watchTarget, broadcast);
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.close();
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/sqlite-adapter.ts
669
- import { createRequire } from "module";
670
- var _require = createRequire(import.meta.url);
671
- var isBun = "bun" in process.versions;
672
- function openSQLite(path) {
673
- if (isBun) {
674
- const { Database } = _require("bun:sqlite");
675
- return new Database(path);
676
- }
677
- const { DatabaseSync } = _require("node:sqlite");
678
- return new DatabaseSync(path);
679
- }
680
- function lastInsertId(db) {
681
- const row = db.prepare("SELECT last_insert_rowid() AS id").get();
682
- return row.id;
683
- }
684
-
685
- // src/core/db.ts
686
- var SCHEMA = `
687
- CREATE TABLE IF NOT EXISTS tasks (
688
- id INTEGER PRIMARY KEY AUTOINCREMENT,
689
- slug TEXT NOT NULL UNIQUE,
690
- title TEXT NOT NULL,
691
- description TEXT,
692
- status TEXT NOT NULL DEFAULT 'pending'
693
- CHECK(status IN ('pending','in_progress','done','blocked')),
694
- assigned_to TEXT,
695
- created_at TEXT NOT NULL,
696
- started_at TEXT,
697
- completed_at TEXT
698
- );
699
-
700
- CREATE TABLE IF NOT EXISTS task_acceptance (
701
- id INTEGER PRIMARY KEY AUTOINCREMENT,
702
- task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
703
- criterion TEXT NOT NULL,
704
- met INTEGER NOT NULL DEFAULT 0
705
- );
706
-
707
- CREATE TABLE IF NOT EXISTS actions (
708
- id TEXT PRIMARY KEY,
709
- task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
710
- agent TEXT NOT NULL
711
- CHECK(agent IN ('lead','explorer','builder','reviewer') OR agent LIKE 'custom:%'),
712
- status TEXT NOT NULL DEFAULT 'in_progress'
713
- CHECK(status IN ('in_progress','completed','blocked')),
714
- created_at TEXT NOT NULL,
715
- completed_at TEXT,
716
- summary TEXT
717
- );
718
-
719
- CREATE TABLE IF NOT EXISTS action_sections (
720
- id INTEGER PRIMARY KEY AUTOINCREMENT,
721
- action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
722
- section_type TEXT NOT NULL,
723
- content TEXT NOT NULL,
724
- created_at TEXT NOT NULL
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
- CREATE TABLE IF NOT EXISTS action_files (
728
- id INTEGER PRIMARY KEY AUTOINCREMENT,
729
- action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
730
- file_path TEXT NOT NULL,
731
- operation TEXT NOT NULL
732
- CHECK(operation IN ('read','created','modified','deleted')),
733
- notes TEXT
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
- CREATE TABLE IF NOT EXISTS action_tools (
737
- id INTEGER PRIMARY KEY AUTOINCREMENT,
738
- action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
739
- tool_name TEXT NOT NULL,
740
- args_json TEXT,
741
- result_summary TEXT,
742
- called_at TEXT NOT NULL
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
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
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
- db;
967
+ tasks;
968
+ actions;
969
+ stats;
970
+ driver;
754
971
  config;
755
- constructor(dbPath, config) {
972
+ constructor(driver, config) {
973
+ this.driver = driver;
756
974
  this.config = config;
757
- const abs = resolve4(dbPath);
758
- mkdirSync5(dirname3(abs), { recursive: true });
759
- this.db = openSQLite(abs);
760
- this.db.exec(`PRAGMA journal_mode = WAL`);
761
- this.db.exec(`PRAGMA foreign_keys = ON`);
762
- this.db.exec(SCHEMA);
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 ?? null,
774
- created_at: now
984
+ description: params.description
775
985
  });
776
- const taskId = lastInsertId(this.db);
777
986
  if (params.acceptance?.length) {
778
- const accStmt = this.db.prepare(
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.getTaskById(taskId);
989
+ await this.regenerateCurrentMd();
990
+ return await this.tasks.getById(taskId);
787
991
  }
788
- getTasks(status) {
789
- if (status) {
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.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(id) ?? null;
995
+ async getTaskById(id) {
996
+ return this.tasks.getById(id);
796
997
  }
797
- getTaskBySlug(slug) {
798
- return this.db.prepare(`SELECT * FROM tasks WHERE slug = ?`).get(slug) ?? null;
998
+ async getTaskBySlug(slug) {
999
+ return this.tasks.getBySlug(slug);
799
1000
  }
800
- getTaskAcceptance(taskId) {
801
- return this.db.prepare(`SELECT * FROM task_acceptance WHERE task_id = ?`).all(taskId);
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.getTaskById(idOrSlug) : this.getTaskBySlug(idOrSlug);
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.db.prepare(
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.db.prepare(
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.db.prepare(`UPDATE tasks SET status = ? WHERE id = ?`).run(status, task2.id);
1013
+ await this.tasks.setStatus(task2.id, status);
817
1014
  }
818
- this.regenerateCurrentMd();
819
- return this.getTaskById(task2.id);
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.db.exec("BEGIN IMMEDIATE");
824
- try {
825
- this.db.prepare(
826
- `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?
827
- WHERE id = ? AND status = 'pending'`
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
- } catch (err) {
835
- this.db.exec("ROLLBACK");
836
- throw err;
837
- }
1028
+ });
838
1029
  }
839
- // ─── Actions ──────────────────────────────────────────────────────────────
840
- startAction(taskId, agent) {
841
- const now = (/* @__PURE__ */ new Date()).toISOString();
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.db.prepare(
853
- `INSERT INTO action_sections (action_id, section_type, content, created_at)
854
- VALUES (?, ?, ?, ?)`
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
- completeAction(actionId, summary) {
1044
+ async writeSection(actionId, sectionType, content) {
859
1045
  const now = (/* @__PURE__ */ new Date()).toISOString();
860
- this.db.prepare(
861
- `UPDATE actions SET status = 'completed', completed_at = ?, summary = ?
862
- WHERE id = ?`
863
- ).run(now, summary, actionId);
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
- const result = this.db.prepare(
870
- `UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done'
871
- WHERE task_id = ? AND status = 'in_progress'`
872
- ).run(now, taskId);
873
- return result.changes;
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.db.prepare(
895
- `INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at)
896
- VALUES (?, ?, ?, ?, ?)`
897
- ).run(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
898
- }
899
- getFilesForTask(taskId) {
900
- return this.db.prepare(
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();
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.getTasks("in_progress");
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.getTasks("pending");
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 actions = this.getActionsForTask(task2.id);
962
- if (actions.length > 0) {
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 actions) {
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.getTaskAcceptance(task2.id);
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 (dashboard / analytics) ───────────────────────────────────
994
- queryRaw(sql, ...params) {
995
- return this.db.prepare(sql).all(...params);
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.getTasks(),
1001
- actions: this.db.prepare(`SELECT * FROM actions ORDER BY created_at`).all(),
1002
- sections: this.db.prepare(`SELECT * FROM action_sections ORDER BY created_at`).all()
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.db.close();
1166
+ async close() {
1167
+ await this.driver.close();
1007
1168
  }
1008
1169
  // ─── feature_list.json sync ───────────────────────────────────────────────
1009
- syncFromFeatureList(tasks) {
1170
+ async syncFromFeatureList(seeds) {
1010
1171
  let added = 0;
1011
1172
  let skipped = 0;
1012
- for (const t of tasks) {
1013
- if (this.getTaskBySlug(t.slug)) {
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
- markAcceptanceMet(criterionId) {
1023
- this.db.prepare(`UPDATE task_acceptance SET met = 1 WHERE id = ?`).run(criterionId);
1024
- }
1025
- writeFeatureList(cwd2) {
1026
- const tasks = this.getTasks();
1027
- const list = tasks.map((t) => ({
1028
- slug: t.slug,
1029
- title: t.title,
1030
- description: t.description ?? void 0,
1031
- acceptance: this.getTaskAcceptance(t.id).map((a) => a.criterion),
1032
- status: t.status
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 dbPath = join6(resolve4(cwd2), config.storage.dbPath);
1041
- return new HarnessDB(dbPath, config);
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.storage.dbPath);
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
- const dbPath = resolve6(cwd2, config.storage.dbPath);
1116
- const dbOk = existsSync6(dbPath);
1117
- checkLine("checking DB", dbOk, `${config.storage.dbPath} reachable`);
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 p2 from "@clack/prompts";
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
- const val = await p2.text({
1271
- message: "Project name",
1272
- placeholder: "my-app",
1273
- ...detectedName && { initialValue: detectedName },
1274
- validate: (v) => v.trim() ? void 0 : "Project name is required"
1275
- });
1276
- if (p2.isCancel(val)) {
1277
- p2.cancel("Cancelled.");
1278
- process.exit(0);
1279
- }
1280
- name = val;
1281
- }
1282
- const descVal = await p2.text({
1283
- message: "Short description (shown to agents as context)",
1284
- placeholder: "A REST API for managing notes"
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 = descVal.trim() || name;
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 p2.select({
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 (p2.isCancel(val)) {
1303
- p2.cancel("Cancelled.");
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 p2.confirm({
1549
+ const globalVal = await p3.confirm({
1310
1550
  message: "Install globally (to home directory)?",
1311
1551
  initialValue: false
1312
1552
  });
1313
- if (p2.isCancel(globalVal)) {
1314
- p2.cancel("Cancelled.");
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
- const val = await p2.text({
1325
- message: "Docs folder path (agents will search here)",
1326
- initialValue: "./docs"
1327
- });
1328
- if (p2.isCancel(val)) {
1329
- p2.cancel("Cancelled.");
1330
- process.exit(0);
1331
- }
1332
- docsPath = val.trim() || "./docs";
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 p2.select({
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 (p2.isCancel(val)) {
1347
- p2.cancel("Cancelled");
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 p2.confirm({ message: "Add your first task now?", initialValue: true });
1353
- if (p2.isCancel(addFirstTask)) {
1354
- p2.cancel("Cancelled");
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 titleVal = await p2.text({
1360
- message: "Task title",
1361
- validate: (v) => v.trim() ? void 0 : "Title is required"
1362
- });
1363
- if (p2.isCancel(titleVal)) {
1364
- p2.cancel("Cancelled");
1365
- process.exit(0);
1366
- }
1367
- const taskTitle = titleVal.trim();
1368
- const taskDescVal = await p2.text({
1369
- message: "Task description",
1370
- placeholder: "What and why"
1371
- });
1372
- if (p2.isCancel(taskDescVal)) {
1373
- p2.cancel("Cancelled");
1374
- process.exit(0);
1375
- }
1376
- const taskDesc = taskDescVal.trim();
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
- p2.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
1627
+ p3.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
1379
1628
  while (true) {
1380
- const criterionVal = await p2.text({
1629
+ const criterionVal = await p3.text({
1381
1630
  message: ">",
1382
1631
  placeholder: "Criterion (or press Enter to finish)"
1383
1632
  });
1384
- if (p2.isCancel(criterionVal) || !criterionVal.trim()) break;
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 = p2.spinner();
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
- p2.log.error(err instanceof Error ? err.message : String(err));
1428
- process.exit(1);
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 p3 from "@clack/prompts";
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 p3.select({
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 (p3.isCancel(val)) {
1468
- p3.cancel("Cancelled.");
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 = p3.spinner();
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
- p3.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
1484
- p3.log.warn(`Then run: ahk build`);
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
- p3.log.error(err instanceof Error ? err.message : String(err));
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 p4 from "@clack/prompts";
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 p4.confirm({
1771
+ const confirm3 = await p5.confirm({
1522
1772
  message: `Remove ${file}?`,
1523
1773
  initialValue: true
1524
1774
  });
1525
- if (p4.isCancel(confirm3)) {
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, storageDir, "harness.db");
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
- const confirm3 = await p4.confirm({
1561
- message: `Delete database (${storageDir}/harness.db)?`,
1562
- initialValue: true
1563
- });
1564
- if (p4.isCancel(confirm3)) {
1565
- console.log(pc8.red(" Cancelled by user."));
1566
- return;
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 p4.confirm({
1832
+ const confirm3 = await p5.confirm({
1576
1833
  message: `Delete feature list (${storageDir}/feature_list.json)?`,
1577
1834
  initialValue: true
1578
1835
  });
1579
- if (p4.isCancel(confirm3)) {
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
- let changed = false;
1590
- if (resetDb) {
1846
+ if (resetDb && dbPath) {
1591
1847
  try {
1592
1848
  rmSync(dbPath, { force: true });
1593
- console.log(pc8.green(` \u2713 Removed ${storageDir}/harness.db`));
1594
- changed = true;
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 = actions.map((a) => ({
1827
- ...a,
1828
- sections: db.getActionSections(a.id)
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 v = args[key];
1931
- if (typeof v !== "string") throw new Error(`${key} must be a string`);
1932
- return v;
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 v = args[key];
1936
- if (typeof v !== "number") throw new Error(`${key} must be a number`);
1937
- return v;
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 = tasks.map((t) => ({
1968
- ...t,
1969
- actions: db.getActionsForTask(t.id),
1970
- acceptance: db.getTaskAcceptance(t.id)
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 p5 from "@clack/prompts";
2359
+ import * as p6 from "@clack/prompts";
2074
2360
  import pc11 from "picocolors";
2075
2361
  async function runTaskAdd(cwd2) {
2076
- p5.intro(pc11.bold("agent-harness-kit \u2014 add task"));
2077
- const titleVal = await p5.text({
2078
- message: "Task title",
2079
- validate: (v) => v.trim() ? void 0 : "Title is required"
2080
- });
2081
- if (p5.isCancel(titleVal)) {
2082
- p5.cancel("Cancelled.");
2083
- process.exit(0);
2084
- }
2085
- const title = titleVal.trim();
2086
- const descVal = await p5.text({
2087
- message: "Description (what and why)",
2088
- placeholder: "Optional"
2089
- });
2090
- if (p5.isCancel(descVal)) {
2091
- p5.cancel("Cancelled.");
2092
- process.exit(0);
2093
- }
2094
- const description = descVal.trim();
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
- p5.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
2389
+ p6.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
2097
2390
  while (true) {
2098
- const val = await p5.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
2099
- if (p5.isCancel(val) || !val || !val.trim()) break;
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 = p5.spinner();
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
- p5.log.error(err instanceof Error ? err.message : String(err));
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 as createRequire2 } from "module";
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 = createRequire2(import.meta.url);
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 = (v) => v.split(".").map(Number);
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;