@cardor/agent-harness-kit 0.17.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
1043
+ }
1044
+ async writeSection(actionId, sectionType, content) {
1045
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1046
+ await this.actions.addSection(actionId, sectionType, content, now);
1047
+ await this.regenerateCurrentMd();
857
1048
  }
858
- completeAction(actionId, summary) {
1049
+ async completeAction(actionId, summary) {
859
1050
  const now = (/* @__PURE__ */ new Date()).toISOString();
860
- this.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) {
1051
+ await this.actions.complete(actionId, summary, now);
1052
+ await this.regenerateCurrentMd();
1053
+ return await this.actions.getById(actionId);
1054
+ }
1055
+ async closeOrphanedActions(taskId) {
868
1056
  const now = (/* @__PURE__ */ new Date()).toISOString();
869
- 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) {
1057
+ return this.actions.closeOrphaned(taskId, now);
1058
+ }
1059
+ async getAction(actionId) {
1060
+ return this.actions.getById(actionId);
1061
+ }
1062
+ async getActionsForTask(taskId) {
1063
+ return this.actions.getForTask(taskId);
1064
+ }
1065
+ async getActionSections(actionId) {
1066
+ return this.actions.getSections(actionId);
1067
+ }
1068
+ async recordFile(actionId, filePath, operation, notes) {
1069
+ return this.actions.addFile(actionId, filePath, operation, notes ?? null);
1070
+ }
1071
+ async recordTool(actionId, toolName, argsJson, resultSummary) {
893
1072
  const now = (/* @__PURE__ */ new Date()).toISOString();
894
- this.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();
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,12 +1347,62 @@ async function runHealth(cwd2) {
1166
1347
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
1167
1348
  import { homedir } from "os";
1168
1349
  import { join as join10 } from "path";
1169
- import * as p2 from "@clack/prompts";
1350
+ import * as p3 from "@clack/prompts";
1170
1351
  import pc6 from "picocolors";
1171
1352
 
1172
- // src/commands/init-helpers.ts
1173
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1174
- import { join as join9 } from "path";
1353
+ // src/schema/init.ts
1354
+ import * as v from "valibot";
1355
+ var initNameSchema = v.pipe(
1356
+ v.string(),
1357
+ v.nonEmpty("Project name is required"),
1358
+ v.minLength(3, "Project name must be at least 3 characters"),
1359
+ v.maxLength(50, "Project name must be at most 50 characters"),
1360
+ v.trim()
1361
+ );
1362
+ var initDescriptionSchema = v.pipe(
1363
+ v.string(),
1364
+ v.nonEmpty("Description is required"),
1365
+ v.maxLength(300, "Description must be at most 300 characters"),
1366
+ v.trim()
1367
+ );
1368
+ var initDocsSchema = v.pipe(
1369
+ v.string(),
1370
+ v.nonEmpty("Docs folder path is required"),
1371
+ v.regex(/^[\w\-./]+$/, "Docs folder path can only contain letters, numbers, dashes, underscores, dots, and slashes"),
1372
+ v.trim()
1373
+ );
1374
+
1375
+ // src/schema/task.ts
1376
+ import * as v2 from "valibot";
1377
+ var taskTitleSchema = v2.pipe(
1378
+ v2.string(),
1379
+ v2.nonEmpty("Task title is required"),
1380
+ v2.minLength(3, "Task title must be at least 3 characters"),
1381
+ v2.maxLength(100, "Task title must be at most 100 characters")
1382
+ );
1383
+ var taskDescriptionSchema = v2.pipe(
1384
+ v2.string(),
1385
+ v2.nonEmpty("Description is required"),
1386
+ v2.maxLength(1e3, "Description must be at most 1000 characters")
1387
+ );
1388
+
1389
+ // src/utils/form.ts
1390
+ import * as p2 from "@clack/prompts";
1391
+ import * as v3 from "valibot";
1392
+ var cliFormWithRetry = async (formFn, schema) => {
1393
+ while (true) {
1394
+ const res = await formFn();
1395
+ const result = v3.safeParse(schema, res);
1396
+ if (result.success) return result.output;
1397
+ const messages = result.issues.map((i) => i.message).join(", ");
1398
+ p2.log.error(messages);
1399
+ p2.log.info("Please try again.\n");
1400
+ }
1401
+ };
1402
+
1403
+ // src/commands/init-helpers.ts
1404
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1405
+ import { join as join9 } from "path";
1175
1406
  import pc5 from "picocolors";
1176
1407
  function readProjectNameFromPackageJson(cwd2) {
1177
1408
  try {
@@ -1202,9 +1433,9 @@ function applyConfigDefaults(params) {
1202
1433
  reviewer: { instructionsPath: null },
1203
1434
  custom: []
1204
1435
  },
1436
+ database: { type: "sqlite", path: ".harness/harness.db" },
1205
1437
  storage: {
1206
1438
  dir: ".harness",
1207
- dbPath: ".harness/harness.db",
1208
1439
  tasks: { adapter: params.tasksAdapter },
1209
1440
  sections: {
1210
1441
  toolsUsed: true,
@@ -1267,50 +1498,60 @@ async function runInit(cwd2, flags) {
1267
1498
  if (flags.name) {
1268
1499
  name = flags.name;
1269
1500
  } else {
1270
- const val = await p2.text({
1271
- message: "Project name",
1272
- placeholder: "my-app",
1273
- validate: (v) => v.trim() ? void 0 : "Project name is required"
1274
- });
1275
- if (p2.isCancel(val)) {
1276
- p2.cancel("Cancelled.");
1277
- process.exit(0);
1278
- }
1279
- name = val;
1280
- }
1281
- const descVal = await p2.text({
1282
- message: "Short description (shown to agents as context)",
1283
- placeholder: "A REST API for managing notes"
1284
- });
1285
- if (p2.isCancel(descVal)) {
1286
- p2.cancel("Cancelled.");
1287
- process.exit(0);
1501
+ name = await cliFormWithRetry(
1502
+ async () => {
1503
+ const val = await p3.text({
1504
+ message: "Project name",
1505
+ placeholder: "my-app",
1506
+ ...detectedName && { initialValue: detectedName }
1507
+ });
1508
+ if (p3.isCancel(val)) {
1509
+ p3.cancel("Cancelled.");
1510
+ process.exit(0);
1511
+ }
1512
+ return val;
1513
+ },
1514
+ initNameSchema
1515
+ );
1288
1516
  }
1289
- const description = 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
+ );
1290
1531
  let provider;
1291
1532
  if (flags.provider && ["claude-code", "opencode"].includes(flags.provider)) {
1292
1533
  provider = flags.provider;
1293
1534
  } else {
1294
- const val = await p2.select({
1535
+ const val = await p3.select({
1295
1536
  message: "AI provider",
1296
1537
  options: [
1297
1538
  { value: "claude-code", label: "Claude Code" },
1298
1539
  { value: "opencode", label: "OpenCode" }
1299
1540
  ]
1300
1541
  });
1301
- if (p2.isCancel(val)) {
1302
- p2.cancel("Cancelled.");
1542
+ if (p3.isCancel(val)) {
1543
+ p3.cancel("Cancelled.");
1303
1544
  process.exit(0);
1304
1545
  }
1305
1546
  provider = val;
1306
1547
  }
1307
1548
  let globalInstallation = false;
1308
- const globalVal = await p2.confirm({
1549
+ const globalVal = await p3.confirm({
1309
1550
  message: "Install globally (to home directory)?",
1310
1551
  initialValue: false
1311
1552
  });
1312
- if (p2.isCancel(globalVal)) {
1313
- p2.cancel("Cancelled.");
1553
+ if (p3.isCancel(globalVal)) {
1554
+ p3.cancel("Cancelled.");
1314
1555
  process.exit(0);
1315
1556
  }
1316
1557
  if (globalVal) {
@@ -1320,21 +1561,26 @@ async function runInit(cwd2, flags) {
1320
1561
  if (flags.docs) {
1321
1562
  docsPath = flags.docs;
1322
1563
  } else {
1323
- const val = await p2.text({
1324
- message: "Docs folder path (agents will search here)",
1325
- initialValue: "./docs"
1326
- });
1327
- if (p2.isCancel(val)) {
1328
- p2.cancel("Cancelled.");
1329
- process.exit(0);
1330
- }
1331
- 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
+ );
1332
1578
  }
1333
1579
  let tasksAdapter;
1334
1580
  if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
1335
1581
  tasksAdapter = flags.tasks;
1336
1582
  } else {
1337
- const val = await p2.select({
1583
+ const val = await p3.select({
1338
1584
  message: "Task adapter",
1339
1585
  options: [
1340
1586
  { value: "local", label: "Local (feature_list.json)" },
@@ -1342,50 +1588,54 @@ async function runInit(cwd2, flags) {
1342
1588
  { value: "linear", label: "Linear (coming soon)" }
1343
1589
  ]
1344
1590
  });
1345
- if (p2.isCancel(val)) {
1346
- p2.cancel("Cancelled");
1591
+ if (p3.isCancel(val)) {
1592
+ p3.cancel("Cancelled");
1347
1593
  process.exit(0);
1348
1594
  }
1349
1595
  tasksAdapter = val;
1350
1596
  }
1351
- const addFirstTask = await p2.confirm({ message: "Add your first task now?", initialValue: true });
1352
- if (p2.isCancel(addFirstTask)) {
1353
- 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");
1354
1600
  process.exit(0);
1355
1601
  }
1356
1602
  let firstTask;
1357
1603
  if (addFirstTask) {
1358
- const titleVal = await p2.text({
1359
- message: "Task title",
1360
- validate: (v) => v.trim() ? void 0 : "Title is required"
1361
- });
1362
- if (p2.isCancel(titleVal)) {
1363
- p2.cancel("Cancelled");
1364
- process.exit(0);
1365
- }
1366
- const taskTitle = titleVal.trim();
1367
- const taskDescVal = await p2.text({
1368
- message: "Task description",
1369
- placeholder: "What and why"
1370
- });
1371
- if (p2.isCancel(taskDescVal)) {
1372
- p2.cancel("Cancelled");
1373
- process.exit(0);
1374
- }
1375
- 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
+ );
1376
1626
  const acceptance = [];
1377
- 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");
1378
1628
  while (true) {
1379
- const criterionVal = await p2.text({
1629
+ const criterionVal = await p3.text({
1380
1630
  message: ">",
1381
1631
  placeholder: "Criterion (or press Enter to finish)"
1382
1632
  });
1383
- if (p2.isCancel(criterionVal) || !criterionVal.trim()) break;
1633
+ if (p3.isCancel(criterionVal) || !criterionVal.trim()) break;
1384
1634
  acceptance.push(criterionVal.trim());
1385
1635
  }
1386
1636
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
1387
1637
  }
1388
- const spinner5 = p2.spinner();
1638
+ const spinner5 = p3.spinner();
1389
1639
  spinner5.start("Scaffolding...");
1390
1640
  try {
1391
1641
  const config = applyConfigDefaults({ name, description, provider, docsPath, tasksAdapter });
@@ -1408,23 +1658,23 @@ async function runInit(cwd2, flags) {
1408
1658
  });
1409
1659
  writeFileSync7(join10(installDir, "agent-harness-kit.config.ts"), configContent, "utf8");
1410
1660
  mkdirSync6(join10(installDir, config.storage.dir), { recursive: true });
1411
- const db = openDB(config, installDir);
1661
+ const db = await openDB(config, installDir);
1412
1662
  await materializer.scaffold(config, { cwd: installDir, firstTask });
1413
1663
  if (firstTask) {
1414
1664
  const slug = slugify(firstTask.title);
1415
- db.addTask({
1665
+ await db.addTask({
1416
1666
  slug,
1417
1667
  title: firstTask.title,
1418
1668
  description: firstTask.description,
1419
1669
  acceptance: firstTask.acceptance
1420
1670
  });
1421
1671
  }
1422
- db.close();
1672
+ await db.close();
1423
1673
  spinner5.stop("");
1424
1674
  } catch (err) {
1425
1675
  spinner5.stop("Failed");
1426
- p2.log.error(err instanceof Error ? err.message : String(err));
1427
- process.exit(1);
1676
+ p3.log.error(err instanceof Error ? err.message : String(err));
1677
+ throw err;
1428
1678
  }
1429
1679
  const agentHarnessKitDir = globalInstallation ? "home directory" : "current directory";
1430
1680
  console.log(pc6.green(`\u2713 Scaffolded harness in ${agentHarnessKitDir}`));
@@ -1448,7 +1698,7 @@ async function runInit(cwd2, flags) {
1448
1698
  }
1449
1699
 
1450
1700
  // src/commands/migrate.ts
1451
- import * as p3 from "@clack/prompts";
1701
+ import * as p4 from "@clack/prompts";
1452
1702
  import pc7 from "picocolors";
1453
1703
  async function runMigrate(cwd2, opts) {
1454
1704
  const config = await loadConfig(cwd2);
@@ -1456,15 +1706,15 @@ async function runMigrate(cwd2, opts) {
1456
1706
  if (opts.to && ["claude-code", "opencode"].includes(opts.to)) {
1457
1707
  target = opts.to;
1458
1708
  } else {
1459
- const val = await p3.select({
1709
+ const val = await p4.select({
1460
1710
  message: "Migrate to provider",
1461
1711
  options: [
1462
1712
  { value: "claude-code", label: "Claude Code" },
1463
1713
  { value: "opencode", label: "OpenCode" }
1464
1714
  ]
1465
1715
  });
1466
- if (p3.isCancel(val)) {
1467
- p3.cancel("Cancelled.");
1716
+ if (p4.isCancel(val)) {
1717
+ p4.cancel("Cancelled.");
1468
1718
  process.exit(0);
1469
1719
  }
1470
1720
  target = val;
@@ -1473,24 +1723,159 @@ async function runMigrate(cwd2, opts) {
1473
1723
  console.log(pc7.dim(`Already on ${target} \u2014 nothing to migrate.`));
1474
1724
  return;
1475
1725
  }
1476
- const spinner5 = p3.spinner();
1726
+ const spinner5 = p4.spinner();
1477
1727
  spinner5.start(`Migrating from ${config.provider} to ${target}...`);
1478
1728
  try {
1479
1729
  const targetMaterializer = getMaterializer(target);
1480
1730
  await targetMaterializer.build(config, cwd2);
1481
1731
  spinner5.stop(pc7.green(`Migrated to ${target}`));
1482
- p3.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
1483
- 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`);
1484
1734
  } catch (err) {
1485
1735
  spinner5.stop(pc7.red("Migration failed"));
1486
- p3.log.error(err instanceof Error ? err.message : String(err));
1736
+ p4.log.error(err instanceof Error ? err.message : String(err));
1487
1737
  process.exit(1);
1488
1738
  }
1489
1739
  }
1490
1740
 
1491
- // src/core/mcp-server.ts
1492
- import { readdirSync, readFileSync as readFileSync6, statSync } from "fs";
1741
+ // src/commands/reset.ts
1742
+ import { existsSync as existsSync8, readdirSync, rmSync } from "fs";
1493
1743
  import { join as join11, resolve as resolve7 } from "path";
1744
+ import * as p5 from "@clack/prompts";
1745
+ import pc8 from "picocolors";
1746
+ var AGENT_MD_FILES = ["lead", "explorer", "builder", "reviewer"];
1747
+ async function resetAgentMds(cwd2, provider) {
1748
+ const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
1749
+ const agentDirPath = resolve7(cwd2, agentDir);
1750
+ if (!existsSync8(agentDirPath)) {
1751
+ console.log(pc8.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
1752
+ return;
1753
+ }
1754
+ const existingFiles = [];
1755
+ try {
1756
+ const files = readdirSync(agentDirPath);
1757
+ for (const f of files) {
1758
+ if (f.endsWith(".md") && AGENT_MD_FILES.includes(f.replace(".md", ""))) {
1759
+ existingFiles.push(f);
1760
+ }
1761
+ }
1762
+ } catch {
1763
+ console.log(pc8.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
1764
+ return;
1765
+ }
1766
+ if (existingFiles.length === 0) {
1767
+ console.log(pc8.yellow(` No agent MD files found in ${agentDir}/`));
1768
+ return;
1769
+ }
1770
+ for (const file of existingFiles) {
1771
+ const confirm3 = await p5.confirm({
1772
+ message: `Remove ${file}?`,
1773
+ initialValue: true
1774
+ });
1775
+ if (p5.isCancel(confirm3)) {
1776
+ console.log(pc8.red(" Cancelled by user."));
1777
+ return;
1778
+ }
1779
+ if (confirm3) {
1780
+ try {
1781
+ const filePath = join11(agentDirPath, file);
1782
+ rmSync(filePath, { force: true });
1783
+ console.log(pc8.green(` Removed ${file}`));
1784
+ } catch {
1785
+ console.error(pc8.red(` Failed to remove ${file}`));
1786
+ }
1787
+ } else {
1788
+ console.log(pc8.cyan(` Skipped ${file}`));
1789
+ }
1790
+ }
1791
+ }
1792
+ async function runReset(cwd2, opts) {
1793
+ let config;
1794
+ try {
1795
+ config = await loadConfig(cwd2);
1796
+ } catch {
1797
+ console.error(pc8.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
1798
+ process.exit(1);
1799
+ }
1800
+ const storageDir = config.storage.dir || ".harness";
1801
+ const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
1802
+ const featureListPath = resolve7(cwd2, storageDir, "feature_list.json");
1803
+ let resetDb = false;
1804
+ let resetFeatureList = false;
1805
+ let resetAgentMdsFlag = false;
1806
+ if (dbPath && existsSync8(dbPath)) {
1807
+ if (opts.force) {
1808
+ resetDb = true;
1809
+ } else {
1810
+ if (config.database.type !== "sqlite") {
1811
+ console.log(pc8.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
1812
+ resetDb = false;
1813
+ } else {
1814
+ const confirm3 = await p5.confirm({
1815
+ message: `Delete database (${config.database.path})?`,
1816
+ initialValue: true
1817
+ });
1818
+ if (p5.isCancel(confirm3)) {
1819
+ console.log(pc8.red(" Cancelled by user."));
1820
+ return;
1821
+ }
1822
+ resetDb = confirm3;
1823
+ }
1824
+ }
1825
+ } else if (!dbPath) {
1826
+ console.log(pc8.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
1827
+ }
1828
+ if (existsSync8(featureListPath)) {
1829
+ if (opts.force) {
1830
+ resetFeatureList = true;
1831
+ } else {
1832
+ const confirm3 = await p5.confirm({
1833
+ message: `Delete feature list (${storageDir}/feature_list.json)?`,
1834
+ initialValue: true
1835
+ });
1836
+ if (p5.isCancel(confirm3)) {
1837
+ console.log(pc8.red(" Cancelled by user."));
1838
+ return;
1839
+ }
1840
+ resetFeatureList = confirm3;
1841
+ }
1842
+ }
1843
+ if (opts.provider) {
1844
+ resetAgentMdsFlag = true;
1845
+ }
1846
+ if (resetDb && dbPath) {
1847
+ try {
1848
+ rmSync(dbPath, { force: true });
1849
+ rmSync(`${dbPath}-wal`, { force: true });
1850
+ rmSync(`${dbPath}-shm`, { force: true });
1851
+ console.log(pc8.green(` \u2713 Removed ${dbPath}`));
1852
+ } catch {
1853
+ console.error(pc8.red(` \u2717 Failed to remove ${dbPath}`));
1854
+ }
1855
+ }
1856
+ if (resetFeatureList) {
1857
+ try {
1858
+ rmSync(featureListPath, { force: true });
1859
+ console.log(pc8.green(` \u2713 Removed ${storageDir}/feature_list.json`));
1860
+ } catch {
1861
+ console.error(pc8.red(` \u2717 Failed to remove ${featureListPath}`));
1862
+ }
1863
+ }
1864
+ if (resetAgentMdsFlag) {
1865
+ console.log("");
1866
+ await resetAgentMds(cwd2, opts.provider || "claude-code");
1867
+ }
1868
+ if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
1869
+ console.log(pc8.yellow(" Nothing to reset (all items missing or skipped)."));
1870
+ return;
1871
+ }
1872
+ console.log("");
1873
+ console.log(pc8.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
1874
+ }
1875
+
1876
+ // src/core/mcp-server.ts
1877
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
1878
+ import { join as join12, resolve as resolve8 } from "path";
1494
1879
  import { Server } from "@modelcontextprotocol/sdk/server";
1495
1880
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1496
1881
  import {
@@ -1634,6 +2019,24 @@ var TOOLS = [
1634
2019
  required: ["criterionId"]
1635
2020
  }
1636
2021
  },
2022
+ {
2023
+ name: "tasks.add",
2024
+ description: "Create a new task in the harness. Use this when the user describes work in natural language. Infer slug, title, description, and acceptance criteria from the conversation. Ask for missing critical info before calling.",
2025
+ inputSchema: {
2026
+ type: "object",
2027
+ properties: {
2028
+ title: { type: "string", description: "Short human-readable title for the task" },
2029
+ slug: { type: "string", description: "URL-safe identifier (lowercase, hyphens). Auto-derived from title if omitted." },
2030
+ description: { type: "string", description: "Longer description of the task goal" },
2031
+ acceptance: {
2032
+ type: "array",
2033
+ items: { type: "string" },
2034
+ description: "List of acceptance criteria (plain sentences)"
2035
+ }
2036
+ },
2037
+ required: ["title"]
2038
+ }
2039
+ },
1637
2040
  {
1638
2041
  name: "actions.record_tool",
1639
2042
  description: "Record a tool call made during an action. This is the only way to populate the Tools dashboard. Call once per tool invocation.",
@@ -1650,8 +2053,8 @@ var TOOLS = [
1650
2053
  }
1651
2054
  ];
1652
2055
  async function startMcpServer(config, cwd2) {
1653
- const db = openDB(config, cwd2);
1654
- const docsPath = resolve7(cwd2, config.project.docsPath);
2056
+ const db = await openDB(config, cwd2);
2057
+ const docsPath = resolve8(cwd2, config.project.docsPath);
1655
2058
  const server = new Server(
1656
2059
  { name: "agent-harness-kit", version: VERSION },
1657
2060
  { capabilities: { tools: {} } }
@@ -1675,52 +2078,62 @@ async function dispatch(name, args, db, docsPath) {
1675
2078
  case "actions.start": {
1676
2079
  const taskId = num(args, "taskId");
1677
2080
  const agent = str(args, "agent");
1678
- const action = db.startAction(taskId, agent);
2081
+ const action = await db.startAction(taskId, agent);
1679
2082
  return ok(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
1680
2083
  }
1681
2084
  case "actions.write": {
1682
2085
  const actionId = str(args, "actionId");
1683
2086
  const sectionType = str(args, "sectionType");
1684
2087
  const content = str(args, "content");
1685
- db.writeSection(actionId, sectionType, content);
2088
+ await db.writeSection(actionId, sectionType, content);
1686
2089
  return ok(JSON.stringify({ actionId, sectionType, recorded: true }));
1687
2090
  }
1688
2091
  case "actions.complete": {
1689
2092
  const actionId = str(args, "actionId");
1690
2093
  const summary = str(args, "summary");
1691
- const action = db.completeAction(actionId, summary);
2094
+ const action = await db.completeAction(actionId, summary);
1692
2095
  return ok(JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at }));
1693
2096
  }
1694
2097
  case "actions.get": {
1695
2098
  const taskId = num(args, "taskId");
1696
- const actions = db.getActionsForTask(taskId);
1697
- const full = actions.map((a) => ({
1698
- ...a,
1699
- sections: db.getActionSections(a.id)
1700
- }));
2099
+ const actions = await db.getActionsForTask(taskId);
2100
+ const full = await Promise.all(
2101
+ actions.map(async (a) => ({
2102
+ ...a,
2103
+ sections: await db.getActionSections(a.id)
2104
+ }))
2105
+ );
1701
2106
  return ok(JSON.stringify(full, null, 2));
1702
2107
  }
1703
2108
  case "tasks.get": {
1704
2109
  const status = args["status"];
1705
- const tasks = status ? db.getTasks(status) : db.getTasks();
2110
+ const tasks = status ? await db.getTasks(status) : await db.getTasks();
1706
2111
  return ok(JSON.stringify(tasks, null, 2));
1707
2112
  }
1708
2113
  case "tasks.claim": {
1709
2114
  const id = num(args, "id");
1710
2115
  const agent = str(args, "agent");
1711
- const task2 = db.claimTask(id, agent);
2116
+ const task2 = await db.claimTask(id, agent);
1712
2117
  if (!task2) {
1713
2118
  return ok(JSON.stringify({ error: "task_already_claimed", taskId: id }));
1714
2119
  }
1715
2120
  return ok(JSON.stringify(task2));
1716
2121
  }
2122
+ case "tasks.add": {
2123
+ const title = str(args, "title");
2124
+ const slug = args["slug"] ?? slugify(title);
2125
+ const description = args["description"];
2126
+ const acceptance = args["acceptance"];
2127
+ const task2 = await db.addTask({ slug, title, description, acceptance });
2128
+ return ok(JSON.stringify(task2));
2129
+ }
1717
2130
  case "tasks.update": {
1718
2131
  const id = num(args, "id");
1719
2132
  const status = str(args, "status");
1720
2133
  if (status === "done") {
1721
- db.closeOrphanedActions(id);
2134
+ await db.closeOrphanedActions(id);
1722
2135
  }
1723
- const task2 = db.updateTaskStatus(id, status);
2136
+ const task2 = await db.updateTaskStatus(id, status);
1724
2137
  return ok(JSON.stringify(task2));
1725
2138
  }
1726
2139
  case "docs.search": {
@@ -1733,12 +2146,12 @@ async function dispatch(name, args, db, docsPath) {
1733
2146
  const filePath = str(args, "filePath");
1734
2147
  const operation = str(args, "operation");
1735
2148
  const notes = args["notes"];
1736
- db.recordFile(actionId, filePath, operation, notes);
2149
+ await db.recordFile(actionId, filePath, operation, notes);
1737
2150
  return ok(JSON.stringify({ actionId, filePath, operation, recorded: true }));
1738
2151
  }
1739
2152
  case "tasks.acceptance.update": {
1740
2153
  const criterionId = num(args, "criterionId");
1741
- db.markAcceptanceMet(criterionId);
2154
+ await db.markAcceptanceMet(criterionId);
1742
2155
  return ok(JSON.stringify({ criterionId, met: true }));
1743
2156
  }
1744
2157
  case "actions.record_tool": {
@@ -1746,7 +2159,7 @@ async function dispatch(name, args, db, docsPath) {
1746
2159
  const toolName = str(args, "toolName");
1747
2160
  const argsJson = args["argsJson"];
1748
2161
  const resultSummary = args["resultSummary"];
1749
- db.recordTool(actionId, toolName, argsJson, resultSummary);
2162
+ await db.recordTool(actionId, toolName, argsJson, resultSummary);
1750
2163
  return ok(JSON.stringify({ actionId, toolName, recorded: true }));
1751
2164
  }
1752
2165
  default:
@@ -1781,8 +2194,8 @@ function searchDocs(docsPath, query, maxResults = 10) {
1781
2194
  function collectMarkdownFiles(dir) {
1782
2195
  const files = [];
1783
2196
  try {
1784
- for (const entry of readdirSync(dir)) {
1785
- const full = join11(dir, entry);
2197
+ for (const entry of readdirSync2(dir)) {
2198
+ const full = join12(dir, entry);
1786
2199
  const stat = statSync(full);
1787
2200
  if (stat.isDirectory()) {
1788
2201
  files.push(...collectMarkdownFiles(full));
@@ -1798,14 +2211,14 @@ function ok(text3, isError = false) {
1798
2211
  return { content: [{ type: "text", text: text3 }], isError };
1799
2212
  }
1800
2213
  function str(args, key) {
1801
- const v = args[key];
1802
- if (typeof v !== "string") throw new Error(`${key} must be a string`);
1803
- return v;
2214
+ const v4 = args[key];
2215
+ if (typeof v4 !== "string") throw new Error(`${key} must be a string`);
2216
+ return v4;
1804
2217
  }
1805
2218
  function num(args, key) {
1806
- const v = args[key];
1807
- if (typeof v !== "number") throw new Error(`${key} must be a number`);
1808
- return v;
2219
+ const v4 = args[key];
2220
+ if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
2221
+ return v4;
1809
2222
  }
1810
2223
 
1811
2224
  // src/commands/serve.ts
@@ -1821,34 +2234,36 @@ async function runServe(cwd2, opts) {
1821
2234
 
1822
2235
  // src/commands/status.ts
1823
2236
  import Table from "cli-table3";
1824
- import pc8 from "picocolors";
2237
+ import pc9 from "picocolors";
1825
2238
  var STATUS_COLOR = {
1826
- pending: (s) => pc8.dim(s),
1827
- in_progress: (s) => pc8.cyan(s),
1828
- done: (s) => pc8.green(s),
1829
- blocked: (s) => pc8.red(s)
2239
+ pending: (s) => pc9.dim(s),
2240
+ in_progress: (s) => pc9.cyan(s),
2241
+ done: (s) => pc9.green(s),
2242
+ blocked: (s) => pc9.red(s)
1830
2243
  };
1831
2244
  async function runStatus(cwd2, opts) {
1832
2245
  const config = await loadConfig(cwd2);
1833
- const db = openDB(config, cwd2);
2246
+ const db = await openDB(config, cwd2);
1834
2247
  try {
1835
- const tasks = db.getTasks();
1836
- const summary = db.getStatusSummary();
2248
+ const tasks = await db.getTasks();
2249
+ const summary = await db.getStatusSummary();
1837
2250
  if (opts.json) {
1838
- const actions = tasks.map((t) => ({
1839
- ...t,
1840
- actions: db.getActionsForTask(t.id),
1841
- acceptance: db.getTaskAcceptance(t.id)
1842
- }));
2251
+ const actions = await Promise.all(
2252
+ tasks.map(async (t) => ({
2253
+ ...t,
2254
+ actions: await db.getActionsForTask(t.id),
2255
+ acceptance: await db.getTaskAcceptance(t.id)
2256
+ }))
2257
+ );
1843
2258
  console.log(JSON.stringify({ tasks: actions, summary }, null, 2));
1844
2259
  return;
1845
2260
  }
1846
2261
  if (tasks.length === 0) {
1847
- console.log(pc8.dim("No tasks yet. Run: ahk task add"));
2262
+ console.log(pc9.dim("No tasks yet. Run: ahk task add"));
1848
2263
  return;
1849
2264
  }
1850
2265
  const table = new Table({
1851
- head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc8.bold(h)),
2266
+ head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc9.bold(h)),
1852
2267
  style: { head: [], border: [] }
1853
2268
  });
1854
2269
  for (const t of tasks) {
@@ -1866,12 +2281,12 @@ async function runStatus(cwd2, opts) {
1866
2281
  const inProgress = tasks.filter((t) => t.status === "in_progress");
1867
2282
  if (inProgress.length > 0) {
1868
2283
  console.log("");
1869
- console.log(pc8.bold("Active actions:"));
2284
+ console.log(pc9.bold("Active actions:"));
1870
2285
  for (const t of inProgress) {
1871
- const actions = db.getActionsForTask(t.id);
2286
+ const actions = await db.getActionsForTask(t.id);
1872
2287
  const active = actions.filter((a) => a.status === "in_progress");
1873
2288
  for (const a of active) {
1874
- console.log(` ${pc8.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
2289
+ console.log(` ${pc9.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
1875
2290
  }
1876
2291
  }
1877
2292
  }
@@ -1880,181 +2295,188 @@ async function runStatus(cwd2, opts) {
1880
2295
  const fn = STATUS_COLOR[s.status] ?? ((x) => x);
1881
2296
  return `${fn(s.status)}: ${s.total}`;
1882
2297
  });
1883
- console.log(pc8.dim("Tasks \u2014 ") + parts.join(pc8.dim(" | ")));
2298
+ console.log(pc9.dim("Tasks \u2014 ") + parts.join(pc9.dim(" | ")));
1884
2299
  } finally {
1885
- db.close();
2300
+ await db.close();
1886
2301
  }
1887
2302
  }
1888
2303
 
1889
2304
  // src/commands/sync.ts
1890
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1891
- import { join as join12, resolve as resolve8 } from "path";
1892
- import pc9 from "picocolors";
2305
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2306
+ import { join as join13, resolve as resolve9 } from "path";
2307
+ import pc10 from "picocolors";
1893
2308
  async function runSync(cwd2, opts) {
1894
2309
  const config = await loadConfig(cwd2);
1895
2310
  const direction = opts.direction ?? "both";
1896
- const featureListPath = resolve8(join12(cwd2, config.storage.dir, "feature_list.json"));
1897
- const db = openDB(config, cwd2);
2311
+ const featureListPath = resolve9(join13(cwd2, config.storage.dir, "feature_list.json"));
2312
+ const db = await openDB(config, cwd2);
1898
2313
  try {
1899
2314
  if (direction === "in" || direction === "both") {
1900
2315
  await syncIn(featureListPath, db, opts.dryRun ?? false);
1901
2316
  }
1902
2317
  if (direction === "out" || direction === "both") {
1903
- syncOut(db, cwd2, opts.dryRun ?? false);
2318
+ await syncOut(db, cwd2, opts.dryRun ?? false);
1904
2319
  }
1905
2320
  } finally {
1906
- db.close();
2321
+ await db.close();
1907
2322
  }
1908
2323
  }
1909
2324
  async function syncIn(featureListPath, db, dryRun) {
1910
- if (!existsSync8(featureListPath)) {
1911
- console.log(pc9.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
2325
+ if (!existsSync9(featureListPath)) {
2326
+ console.log(pc10.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
1912
2327
  return;
1913
2328
  }
1914
2329
  let seeds;
1915
2330
  try {
1916
2331
  seeds = JSON.parse(readFileSync7(featureListPath, "utf8"));
1917
2332
  } catch (err) {
1918
- console.error(pc9.red(`Failed to parse feature_list.json: ${err}`));
2333
+ console.error(pc10.red(`Failed to parse feature_list.json: ${err}`));
1919
2334
  process.exit(1);
1920
2335
  }
1921
2336
  if (dryRun) {
1922
- console.log(pc9.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
2337
+ console.log(pc10.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
1923
2338
  for (const t of seeds) {
1924
- const existing = db.getTaskBySlug(t.slug);
1925
- console.log(` ${existing ? pc9.dim("skip") : pc9.green("add ")} ${t.slug}`);
2339
+ const existing = await db.getTaskBySlug(t.slug);
2340
+ console.log(` ${existing ? pc10.dim("skip") : pc10.green("add ")} ${t.slug}`);
1926
2341
  }
1927
2342
  return;
1928
2343
  }
1929
- const result = db.syncFromFeatureList(seeds);
1930
- console.log(pc9.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
2344
+ const result = await db.syncFromFeatureList(seeds);
2345
+ console.log(pc10.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
1931
2346
  }
1932
- function syncOut(db, cwd2, dryRun) {
2347
+ async function syncOut(db, cwd2, dryRun) {
1933
2348
  if (dryRun) {
1934
- const tasks = db.getTasks();
1935
- console.log(pc9.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
2349
+ const tasks = await db.getTasks();
2350
+ console.log(pc10.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
1936
2351
  console.log(` ${tasks.length} tasks would be written`);
1937
2352
  return;
1938
2353
  }
1939
- db.writeFeatureList(cwd2);
1940
- console.log(pc9.green("\u2713 Out-sync: feature_list.json updated"));
2354
+ await db.writeFeatureList(cwd2);
2355
+ console.log(pc10.green("\u2713 Out-sync: feature_list.json updated"));
1941
2356
  }
1942
2357
 
1943
2358
  // src/commands/task/add.ts
1944
- import * as p4 from "@clack/prompts";
1945
- import pc10 from "picocolors";
2359
+ import * as p6 from "@clack/prompts";
2360
+ import pc11 from "picocolors";
1946
2361
  async function runTaskAdd(cwd2) {
1947
- p4.intro(pc10.bold("agent-harness-kit \u2014 add task"));
1948
- const titleVal = await p4.text({
1949
- message: "Task title",
1950
- validate: (v) => v.trim() ? void 0 : "Title is required"
1951
- });
1952
- if (p4.isCancel(titleVal)) {
1953
- p4.cancel("Cancelled.");
1954
- process.exit(0);
1955
- }
1956
- const title = titleVal.trim();
1957
- const descVal = await p4.text({
1958
- message: "Description (what and why)",
1959
- placeholder: "Optional"
1960
- });
1961
- if (p4.isCancel(descVal)) {
1962
- p4.cancel("Cancelled.");
1963
- process.exit(0);
1964
- }
1965
- 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
+ );
1966
2388
  const acceptance = [];
1967
- p4.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");
1968
2390
  while (true) {
1969
- const val = await p4.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
1970
- if (p4.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;
1971
2393
  acceptance.push(val.trim());
1972
2394
  }
1973
- const spinner5 = p4.spinner();
2395
+ const spinner5 = p6.spinner();
1974
2396
  spinner5.start("Saving...");
1975
2397
  try {
1976
2398
  const config = await loadConfig(cwd2);
1977
- const db = openDB(config, cwd2);
2399
+ const db = await openDB(config, cwd2);
1978
2400
  const slug = slugify(title);
1979
- const task2 = db.addTask({ slug, title, description: description || void 0, acceptance });
1980
- db.writeFeatureList(cwd2);
1981
- db.close();
2401
+ const task2 = await db.addTask({ slug, title, description: description || void 0, acceptance });
2402
+ await db.writeFeatureList(cwd2);
2403
+ await db.close();
1982
2404
  spinner5.stop("");
1983
- console.log(pc10.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
1984
- console.log(pc10.cyan("\u2192") + " " + pc10.cyan("ahk status") + " to see all tasks");
2405
+ console.log(pc11.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
2406
+ console.log(pc11.cyan("\u2192") + " " + pc11.cyan("ahk status") + " to see all tasks");
1985
2407
  } catch (err) {
1986
- spinner5.stop(pc10.red("Failed"));
1987
- p4.log.error(err instanceof Error ? err.message : String(err));
2408
+ spinner5.stop(pc11.red("Failed"));
2409
+ p6.log.error(err instanceof Error ? err.message : String(err));
1988
2410
  process.exit(1);
1989
2411
  }
1990
2412
  }
1991
2413
 
1992
2414
  // src/commands/task/done.ts
1993
2415
  import { spawnSync as spawnSync2 } from "child_process";
1994
- import { existsSync as existsSync9 } from "fs";
1995
- import { resolve as resolve9 } from "path";
1996
- import pc11 from "picocolors";
2416
+ import { existsSync as existsSync10 } from "fs";
2417
+ import { resolve as resolve10 } from "path";
2418
+ import pc12 from "picocolors";
1997
2419
  async function runTaskDone(cwd2, idOrSlug) {
1998
2420
  const config = await loadConfig(cwd2);
1999
2421
  if (config.health.required) {
2000
- const scriptPath = resolve9(cwd2, config.health.scriptPath);
2001
- if (existsSync9(scriptPath)) {
2422
+ const scriptPath = resolve10(cwd2, config.health.scriptPath);
2423
+ if (existsSync10(scriptPath)) {
2002
2424
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
2003
2425
  if (result.status !== 0) {
2004
- console.error(pc11.red("\u2717 Health check failed \u2014 cannot mark task as done."));
2426
+ console.error(pc12.red("\u2717 Health check failed \u2014 cannot mark task as done."));
2005
2427
  if (result.stdout) console.error(result.stdout);
2006
2428
  if (result.stderr) console.error(result.stderr);
2007
2429
  process.exit(1);
2008
2430
  }
2009
2431
  }
2010
2432
  }
2011
- const db = openDB(config, cwd2);
2433
+ const db = await openDB(config, cwd2);
2012
2434
  try {
2013
2435
  const parsed = parseInt(idOrSlug, 10);
2014
2436
  const isId = !isNaN(parsed);
2015
- const task2 = isId ? db.getTaskById(parsed) : db.getTaskBySlug(idOrSlug);
2437
+ const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
2016
2438
  if (!task2) {
2017
- console.error(pc11.red(`Task not found: ${idOrSlug}`));
2439
+ console.error(pc12.red(`Task not found: ${idOrSlug}`));
2018
2440
  process.exit(1);
2019
2441
  }
2020
2442
  if (task2.status === "done") {
2021
- console.log(pc11.dim(`Task #${task2.id} is already done.`));
2443
+ console.log(pc12.dim(`Task #${task2.id} is already done.`));
2022
2444
  return;
2023
2445
  }
2024
- db.updateTaskStatus(task2.id, "done");
2025
- db.writeFeatureList(cwd2);
2026
- console.log(pc11.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
2446
+ await db.updateTaskStatus(task2.id, "done");
2447
+ await db.writeFeatureList(cwd2);
2448
+ console.log(pc12.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
2027
2449
  } finally {
2028
- db.close();
2450
+ await db.close();
2029
2451
  }
2030
2452
  }
2031
2453
 
2032
2454
  // src/commands/task/list.ts
2033
2455
  import Table2 from "cli-table3";
2034
- import pc12 from "picocolors";
2456
+ import pc13 from "picocolors";
2035
2457
  var STATUS_COLOR2 = {
2036
- pending: (s) => pc12.dim(s),
2037
- in_progress: (s) => pc12.cyan(s),
2038
- done: (s) => pc12.green(s),
2039
- blocked: (s) => pc12.red(s)
2458
+ pending: (s) => pc13.dim(s),
2459
+ in_progress: (s) => pc13.cyan(s),
2460
+ done: (s) => pc13.green(s),
2461
+ blocked: (s) => pc13.red(s)
2040
2462
  };
2041
2463
  async function runTaskList(cwd2, opts) {
2042
2464
  const config = await loadConfig(cwd2);
2043
- const db = openDB(config, cwd2);
2465
+ const db = await openDB(config, cwd2);
2044
2466
  try {
2045
2467
  const validStatuses = ["pending", "in_progress", "done", "blocked"];
2046
2468
  const filterStatus = opts.status && validStatuses.includes(opts.status) ? opts.status : void 0;
2047
- const tasks = filterStatus ? db.getTasks(filterStatus) : db.getTasks();
2469
+ const tasks = filterStatus ? await db.getTasks(filterStatus) : await db.getTasks();
2048
2470
  if (opts.json) {
2049
2471
  console.log(JSON.stringify(tasks, null, 2));
2050
2472
  return;
2051
2473
  }
2052
2474
  if (tasks.length === 0) {
2053
- console.log(pc12.dim("No tasks" + (filterStatus ? ` with status: ${filterStatus}` : "") + "."));
2475
+ console.log(pc13.dim("No tasks" + (filterStatus ? ` with status: ${filterStatus}` : "") + "."));
2054
2476
  return;
2055
2477
  }
2056
2478
  const table = new Table2({
2057
- head: ["ID", "Slug", "Title", "Status"].map((h) => pc12.bold(h)),
2479
+ head: ["ID", "Slug", "Title", "Status"].map((h) => pc13.bold(h)),
2058
2480
  style: { head: [], border: [] }
2059
2481
  });
2060
2482
  for (const t of tasks) {
@@ -2063,20 +2485,20 @@ async function runTaskList(cwd2, opts) {
2063
2485
  }
2064
2486
  console.log(table.toString());
2065
2487
  } finally {
2066
- db.close();
2488
+ await db.close();
2067
2489
  }
2068
2490
  }
2069
2491
 
2070
2492
  // src/core/package-data.ts
2071
- import { createRequire as createRequire2 } from "module";
2072
- import { dirname as dirname5, join as join13 } from "path";
2493
+ import { createRequire } from "module";
2494
+ import { dirname as dirname5, join as join14 } from "path";
2073
2495
  import { fileURLToPath as fileURLToPath3 } from "url";
2074
- var require2 = createRequire2(import.meta.url);
2075
- var pkgPath = join13(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
2496
+ var require2 = createRequire(import.meta.url);
2497
+ var pkgPath = join14(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
2076
2498
  var pkg = require2(pkgPath);
2077
2499
 
2078
2500
  // src/core/update-check.ts
2079
- import pc13 from "picocolors";
2501
+ import pc14 from "picocolors";
2080
2502
  var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
2081
2503
  var TIMEOUT_MS = 2500;
2082
2504
  function checkForUpdate(currentVersion) {
@@ -2094,22 +2516,22 @@ function checkForUpdate(currentVersion) {
2094
2516
  }
2095
2517
  function printUpdateMessage({ current, latest }) {
2096
2518
  const lines = [
2097
- ` Update available ${pc13.dim(current)} \u2192 ${pc13.green(latest)} `,
2098
- ` Run: ${pc13.cyan(`npm i ${pkg.name}@${latest}`)} `
2519
+ ` Update available ${pc14.dim(current)} \u2192 ${pc14.green(latest)} `,
2520
+ ` Run: ${pc14.cyan(`npm i ${pkg.name}@${latest}`)} `
2099
2521
  ];
2100
2522
  const width = Math.max(...lines.map((l) => stripAnsi2(l).length));
2101
2523
  const border = "\u2500".repeat(width);
2102
2524
  console.log();
2103
- console.log(pc13.yellow(`\u250C${border}\u2510`));
2525
+ console.log(pc14.yellow(`\u250C${border}\u2510`));
2104
2526
  for (const line of lines) {
2105
2527
  const pad = width - stripAnsi2(line).length;
2106
- console.log(pc13.yellow("\u2502") + line + " ".repeat(pad) + pc13.yellow("\u2502"));
2528
+ console.log(pc14.yellow("\u2502") + line + " ".repeat(pad) + pc14.yellow("\u2502"));
2107
2529
  }
2108
- console.log(pc13.yellow(`\u2514${border}\u2518`));
2530
+ console.log(pc14.yellow(`\u2514${border}\u2518`));
2109
2531
  console.log();
2110
2532
  }
2111
2533
  function isNewer(latest, current) {
2112
- const toNum = (v) => v.split(".").map(Number);
2534
+ const toNum = (v4) => v4.split(".").map(Number);
2113
2535
  const [lMaj, lMin, lPat] = toNum(latest);
2114
2536
  const [cMaj, cMin, cPat] = toNum(current);
2115
2537
  if (lMaj !== cMaj) return lMaj > cMaj;
@@ -2120,134 +2542,6 @@ function stripAnsi2(str2) {
2120
2542
  return str2.replace(/\x1B\[[0-9;]*m/g, "");
2121
2543
  }
2122
2544
 
2123
- // src/commands/reset.ts
2124
- import { existsSync as existsSync10, readdirSync as readdirSync2, rmSync } from "fs";
2125
- import { join as join14, resolve as resolve10 } from "path";
2126
- import * as p5 from "@clack/prompts";
2127
- import pc14 from "picocolors";
2128
- async function resetAgentMds(cwd2, provider) {
2129
- const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2130
- const agentDirPath = resolve10(cwd2, agentDir);
2131
- if (!existsSync10(agentDirPath)) {
2132
- console.log(pc14.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2133
- return;
2134
- }
2135
- const existingFiles = [];
2136
- try {
2137
- const files = readdirSync2(agentDirPath);
2138
- for (const f of files) {
2139
- if (f.endsWith(".md")) {
2140
- existingFiles.push(f);
2141
- }
2142
- }
2143
- } catch {
2144
- console.log(pc14.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2145
- return;
2146
- }
2147
- if (existingFiles.length === 0) {
2148
- console.log(pc14.yellow(` No agent MD files found in ${agentDir}/`));
2149
- return;
2150
- }
2151
- for (const file of existingFiles) {
2152
- const confirm3 = await p5.confirm({
2153
- message: `Remove ${file}?`,
2154
- initialValue: true
2155
- });
2156
- if (p5.isCancel(confirm3)) {
2157
- console.log(pc14.red(" Cancelled by user."));
2158
- return;
2159
- }
2160
- if (confirm3) {
2161
- try {
2162
- const filePath = join14(agentDirPath, file);
2163
- rmSync(filePath, { force: true });
2164
- console.log(pc14.green(` Removed ${file}`));
2165
- } catch {
2166
- console.error(pc14.red(` Failed to remove ${file}`));
2167
- }
2168
- } else {
2169
- console.log(pc14.cyan(` Skipped ${file}`));
2170
- }
2171
- }
2172
- }
2173
- async function runReset(cwd2, opts) {
2174
- let config;
2175
- try {
2176
- config = await loadConfig(cwd2);
2177
- } catch {
2178
- console.error(pc14.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2179
- process.exit(1);
2180
- }
2181
- const storageDir = config.storage.dir || ".harness";
2182
- const dbPath = resolve10(cwd2, storageDir, "harness.db");
2183
- const featureListPath = resolve10(cwd2, storageDir, "feature_list.json");
2184
- let resetDb = false;
2185
- let resetFeatureList = false;
2186
- let resetAgentMdsFlag = false;
2187
- if (existsSync10(dbPath)) {
2188
- if (opts.force) {
2189
- resetDb = true;
2190
- } else {
2191
- const confirm3 = await p5.confirm({
2192
- message: `Delete database (${storageDir}/harness.db)?`,
2193
- initialValue: true
2194
- });
2195
- if (p5.isCancel(confirm3)) {
2196
- console.log(pc14.red(" Cancelled by user."));
2197
- return;
2198
- }
2199
- resetDb = confirm3;
2200
- }
2201
- }
2202
- if (existsSync10(featureListPath)) {
2203
- if (opts.force) {
2204
- resetFeatureList = true;
2205
- } else {
2206
- const confirm3 = await p5.confirm({
2207
- message: `Delete feature list (${storageDir}/feature_list.json)?`,
2208
- initialValue: true
2209
- });
2210
- if (p5.isCancel(confirm3)) {
2211
- console.log(pc14.red(" Cancelled by user."));
2212
- return;
2213
- }
2214
- resetFeatureList = confirm3;
2215
- }
2216
- }
2217
- if (opts.provider) {
2218
- resetAgentMdsFlag = true;
2219
- }
2220
- let changed = false;
2221
- if (resetDb) {
2222
- try {
2223
- rmSync(dbPath, { force: true });
2224
- console.log(pc14.green(` \u2713 Removed ${storageDir}/harness.db`));
2225
- changed = true;
2226
- } catch {
2227
- console.error(pc14.red(` \u2717 Failed to remove ${dbPath}`));
2228
- }
2229
- }
2230
- if (resetFeatureList) {
2231
- try {
2232
- rmSync(featureListPath, { force: true });
2233
- console.log(pc14.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2234
- changed = true;
2235
- } catch {
2236
- console.error(pc14.red(` \u2717 Failed to remove ${featureListPath}`));
2237
- }
2238
- }
2239
- if (resetAgentMdsFlag) {
2240
- console.log("");
2241
- await resetAgentMds(cwd2, opts.provider || "claude-code");
2242
- }
2243
- if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
2244
- console.log(pc14.yellow(" Nothing to reset (all items missing or skipped)."));
2245
- return;
2246
- }
2247
- console.log("");
2248
- console.log(pc14.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2249
- }
2250
-
2251
2545
  // src/cli.ts
2252
2546
  var cwd = process.cwd();
2253
2547
  var updateCheck = checkForUpdate(pkg.version);