@cardor/agent-harness-kit 1.11.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -10
- package/dist/agent-templates/builder.md +20 -14
- package/dist/agent-templates/explorer.md +11 -8
- package/dist/agent-templates/lead.md +10 -7
- package/dist/agent-templates/reviewer.md +9 -5
- package/dist/{chunk-6PEIJ2D5.js → chunk-JTACLEGM.js} +45 -16
- package/dist/chunk-JTACLEGM.js.map +1 -0
- package/dist/chunk-URMVLD2S.js +147 -0
- package/dist/chunk-URMVLD2S.js.map +1 -0
- package/dist/cli.js +770 -407
- package/dist/cli.js.map +1 -1
- package/dist/{db-3OXHRFAR.js → db-L3AADJF5.js} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/{mysql-THKQOXIS.js → mysql-AUPKARWA.js} +11 -5
- package/dist/mysql-AUPKARWA.js.map +1 -0
- package/dist/{postgres-IOQE32DM.js → postgres-BB4GY4PN.js} +11 -5
- package/dist/postgres-BB4GY4PN.js.map +1 -0
- package/dist/{sqlite-TR4D324R.js → sqlite-5OWKTUUZ.js} +11 -5
- package/dist/sqlite-5OWKTUUZ.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-6PEIJ2D5.js.map +0 -1
- package/dist/mysql-THKQOXIS.js.map +0 -1
- package/dist/postgres-IOQE32DM.js.map +0 -1
- package/dist/sqlite-TR4D324R.js.map +0 -1
- /package/dist/{db-3OXHRFAR.js.map → db-L3AADJF5.js.map} +0 -0
package/README.md
CHANGED
|
@@ -89,7 +89,9 @@ ahk init
|
|
|
89
89
|
|
|
90
90
|
AI tool opens your project
|
|
91
91
|
└── reads .claude/mcp.json, opencode.json, or .codex/config.toml
|
|
92
|
-
└── spawns:
|
|
92
|
+
└── spawns: ahk serve (stdio MCP server)
|
|
93
|
+
via your package manager (npx/pnpm exec/yarn run/bunx) when the
|
|
94
|
+
package is a local dependency, or the bare binary when it isn't
|
|
93
95
|
|
|
94
96
|
Agent starts working
|
|
95
97
|
└── tasks.get() → picks a task from the backlog
|
|
@@ -117,7 +119,7 @@ Everything is stored locally in a SQLite database (`.harness/harness.db`). No cl
|
|
|
117
119
|
- **Markdown fallback** — `current.md` is always regenerated so agents can understand the session state even without the MCP server.
|
|
118
120
|
- **Docs search** — agents can call `docs.search(query)` to find relevant content in your project's docs folder before writing code.
|
|
119
121
|
- **Multi-database support** — SQLite by default (uses `better-sqlite3` on Node ≥ 22 or `bun:sqlite` on Bun). Switch to PostgreSQL or MySQL with a single config line — same schema, same MCP tools, same workflow.
|
|
120
|
-
- **Incremental scaffold** — `ahk init` preserves files you've already customized (agent definitions you've edited are kept). `ahk build`
|
|
122
|
+
- **Incremental scaffold** — `ahk init` preserves files you've already customized (agent definitions you've edited are kept). A hand-written `.harness/feature_list.json` backlog is **merged, never overwritten** — existing tasks survive and any first task you add during init is folded in (deduplicated by slug). `ahk build` also creates missing agent files and never touches existing ones. Use `ahk build --force` to regenerate them from the latest templates, discarding your edits (a backup is written first).
|
|
121
123
|
- **Global installation** — `ahk init` can scaffold the harness into your home directory (`~/.claude` or `~/.config/opencode`) to share it across all projects.
|
|
122
124
|
- **Input validation** — CLI prompts validate all inputs (name length, path format, task title, etc.) and retry with the error message instead of silently accepting bad values.
|
|
123
125
|
|
|
@@ -171,9 +173,14 @@ npx ahk init
|
|
|
171
173
|
| yarn classic (v1) | `packageManager` field (major 1) or `yarn.lock` without `.yarnrc.yml` | `yarn run ahk serve --port <port>` |
|
|
172
174
|
| yarn berry (v2+, PnP or node-modules) | `packageManager` field (major ≥ 2) or `yarn.lock` + `.yarnrc.yml` | `yarn run ahk serve --port <port>` |
|
|
173
175
|
| bun | `packageManager` field or `bun.lockb`/`bun.lock` | `bunx --no-install ahk serve --port <port>` |
|
|
176
|
+
| **any — no local install** | `@cardor/agent-harness-kit` is not a dependency of your project | `ahk serve --port <port>` |
|
|
174
177
|
|
|
175
178
|
Detection order: the `packageManager` field in your `package.json` (e.g. `"packageManager": "pnpm@8.15.0"`) takes priority when present; otherwise `ahk` falls back to lockfile heuristics; if nothing is detected, it defaults to npm.
|
|
176
179
|
|
|
180
|
+
**Global installs bypass the package manager entirely.** Every command in the table above asks your package manager to resolve a *locally installed* `ahk` binary — `npx --no` deliberately refuses to download one, and `pnpm exec`/`yarn run`/`bunx --no-install` have nothing to point at. If you installed the CLI globally and never added it to the project, all five of those commands fail. So `ahk` checks for the local install first (the same check that decides your config file format, above) and, when there is none, generates the bare `ahk serve --port <port>` — resolved from your `PATH` like any other global binary. The package-manager-specific commands are used only when a local install actually exists. If, on that global-install path, `ahk` is not resolvable on your `PATH` at generation time, `ahk` prints a non-blocking warning (the command still succeeds) pointing you at `npm i -g @cardor/agent-harness-kit` or a local install — moving the "binary not found" failure earlier instead of surfacing it later when the MCP server is spawned.
|
|
181
|
+
|
|
182
|
+
Working inside the `agent-harness-kit` repository itself counts as a local install: the package manager can resolve the workspace binary, so the `pnpm exec` form is generated rather than the bare one.
|
|
183
|
+
|
|
177
184
|
**Existing projects:** if you initialized your project before this change, your `.mcp.json`/`opencode.json`/`.codex/config.toml` may still have a hardcoded `npx` command. No migration step is needed — `ahk build` always regenerates (merges) these files from scratch on every run, so the command self-corrects the next time you run `ahk build` (or `ahk build --sync`), including if you've since switched package managers.
|
|
178
185
|
|
|
179
186
|
---
|
|
@@ -184,10 +191,9 @@ Detection order: the `packageManager` field in your `package.json` (e.g. `"packa
|
|
|
184
191
|
|
|
185
192
|
Interactive scaffold. Asks for your project name, description, AI provider, docs path, storage scope, task adapter, and an optional first task. Creates all harness files in the current directory.
|
|
186
193
|
|
|
187
|
-
|
|
194
|
+
Claude Code only, init asks you to pick a model for each of the 5 core roles (lead, explorer, consultant, builder, reviewer) one at a time: `inherit` (default), `haiku`, `sonnet`, `opus`, or `fable`. Each choice is written straight into that role's generated `.claude/agents/<role>.md` frontmatter as a `model:` line at scaffold time — it is never persisted to the config file. Picking `inherit` (the default) emits no `model:` line at all, leaving Claude Code to apply its own default. This prompt only runs during `ahk init`'s one-time scaffold, not on `ahk build` — agent files are user-owned once generated (see [Agent files are yours](#agent-files-are-yours) below), so after init the model is changed the same way as any other edit: hand-editing the `model:` frontmatter line directly.
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
- Codex CLI: free-text model name per agent — Codex does not validate this value; leaving it blank or under 3 characters means no override is written to that agent's TOML file.
|
|
196
|
+
OpenCode and Codex CLI are unaffected by this prompt — it never appears for those providers. OpenCode has no closed model enum to prompt against, and Codex's model is still set by hand-editing `model = "..."` in its TOML.
|
|
191
197
|
|
|
192
198
|
**Storage scope** — where the harness DB (and its `current.md` fallback) physically lives:
|
|
193
199
|
|
|
@@ -227,7 +233,23 @@ ahk build --sync # kept for backwards compatibility — now a no-op on every
|
|
|
227
233
|
|
|
228
234
|
`ahk build` **creates agent files that are missing and never modifies ones that already exist.** Edit `.claude/agents/<role>.md` (or `.opencode/agents/<role>.md`, or `.codex/agents/<role>.toml`) freely — change the role prompt, set a `model:` line, adjust the restriction fields. Rebuilding will not revert your work. `ahk doctor` does not report hand-edited files either; it checks existence only.
|
|
229
235
|
|
|
230
|
-
Everything else `build` writes —
|
|
236
|
+
Everything else `build` writes — MCP config and skills — is derived from your config and **is** regenerated on every run.
|
|
237
|
+
|
|
238
|
+
### `AGENTS.md` and `CLAUDE.md` — derived, but your edits are safe
|
|
239
|
+
|
|
240
|
+
`AGENTS.md` (all providers) and `CLAUDE.md` (Claude Code only) are generated from your config, so a config change should flow into them — but they are also files people hand-edit. `build` reconciles both concerns with a **provenance marker**: every generated file ends with a comment holding a checksum of the exact bytes we wrote, e.g.
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
<!-- ahk:generated 3f7a…c1 -->
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
On each build the marker lets `build` tell its own untouched output apart from a human edit, byte-for-byte:
|
|
247
|
+
|
|
248
|
+
- **Untouched since we wrote it, config changed** → the file is regenerated so your config propagates. No prompt, no backup — it was provably our own output.
|
|
249
|
+
- **Already up to date** → no-op.
|
|
250
|
+
- **You edited the body** (the checksum no longer matches) **or the file has no marker** (written by an older version, e.g. a `CLAUDE.md` you customized before upgrading) → **left untouched**, and `build` prints a loud notice naming the file and telling you to run `--force` if you actually want it regenerated.
|
|
251
|
+
|
|
252
|
+
Because the checksum is over the exact bytes, *any* change — even one space — counts as an edit and is preserved. The behavior is identical with or without a terminal (there is no prompt), so it is safe in scripts and CI. Leave the marker comment in place; deleting it just makes `build` treat the file as hand-edited (preserve it) on the next run.
|
|
231
253
|
|
|
232
254
|
> **If you use OpenCode or Codex CLI, your agent files may be out of date right now.** Those two providers have always preserved existing agent files on build, which means they have never picked up template improvements shipped in newer versions of this package. Claude Code, by contrast, used to overwrite them on every build — that inconsistency was a bug, and it is now fixed in favour of preserving your edits. To pull in the current templates, run `ahk build --force` (read the warning below first).
|
|
233
255
|
|
|
@@ -240,9 +262,11 @@ ahk build --force
|
|
|
240
262
|
```
|
|
241
263
|
|
|
242
264
|
- **It discards your customizations.** Every agent file is rewritten from the template. Prompt edits, `model:` lines, and restriction tweaks are all lost.
|
|
243
|
-
- **It backs up first.** Before overwriting anything, the current content of every affected file is copied
|
|
265
|
+
- **It backs up first.** Before overwriting anything, the current content of every affected file is copied under `.harness/backups/` — agent files to `agents-<timestamp>/`, hand-edited `AGENTS.md`/`CLAUDE.md` to `derived-<timestamp>/`. If that backup cannot be written, the command aborts and **no file is modified** — the same fail-safe as [`ahk migrate storage --force`](#storage-migration).
|
|
244
266
|
- **It names what it touched.** The command prints every file it overwrote and the backup location, so you can diff or restore.
|
|
245
267
|
|
|
268
|
+
`--force` also regenerates a hand-edited `AGENTS.md` or `CLAUDE.md` (backing it up first) — the only time you need it for those files, since an *unedited* one already re-generates on its own when config changes.
|
|
269
|
+
|
|
246
270
|
`--watch` never forces, even if you pass both flags: an automatic rebuild triggered by a file change must not destroy your edits in the background.
|
|
247
271
|
|
|
248
272
|
`--sync` used to rewrite the `tools:` frontmatter of agent files so it matched a canonical allowlist. Agent files no longer declare an allowlist at all — they inherit every tool and declare only restrictions — so there is nothing left to synchronise. Use `ahk build --force` to regenerate agent files.
|
|
@@ -259,7 +283,11 @@ ahk dashboard --port 8080 # custom port
|
|
|
259
283
|
ahk dashboard --no-open # start server without opening browser
|
|
260
284
|
```
|
|
261
285
|
|
|
262
|
-
|
|
286
|
+
`--port` must be an integer between `1` and `65535`; an invalid value (e.g. `ahk dashboard --port abc` or `--port 99999`) is rejected at the CLI with a clear error naming the flag and the valid range, rather than silently failing.
|
|
287
|
+
|
|
288
|
+
If the requested port (default `4242`) is already in use, `ahk dashboard` automatically tries up to 10 sequential ports (e.g. `4242 → 4243 → … → 4251`), printing `Port 4242 in use, using 4243`. The actual port opened is printed to the console. If all 10 ports are exhausted, the command exits with a clear error message showing which port range was attempted.
|
|
289
|
+
|
|
290
|
+
Port availability is checked against the same network interface the dashboard actually binds to, so an already-running `ahk dashboard` — or any other server holding that port — is reliably detected. The success banner is printed only after the server has genuinely bound; if the bind fails (for example, the port was claimed by another process in the moment between the check and the bind), the command reports an actionable error instead of crashing.
|
|
263
291
|
|
|
264
292
|
The dashboard includes:
|
|
265
293
|
|
|
@@ -338,6 +366,8 @@ ahk serve
|
|
|
338
366
|
ahk serve --port 3456 # store a port hint in config (stdio transport only)
|
|
339
367
|
```
|
|
340
368
|
|
|
369
|
+
`--port` must be an integer between `1` and `65535`; an invalid value is rejected at the CLI with a clear error.
|
|
370
|
+
|
|
341
371
|
---
|
|
342
372
|
|
|
343
373
|
### `ahk task add`
|
|
@@ -764,6 +794,8 @@ The equivalent constraint under Claude Code is expressed as `disallowedTools: [W
|
|
|
764
794
|
|
|
765
795
|
The human-editable task backlog. Add tasks here, then run `ahk sync` to load them into SQLite.
|
|
766
796
|
|
|
797
|
+
`ahk init` **never clobbers** this file: an existing backlog is merged into SQLite (deduplicated by slug) alongside any first task you add during init, then re-emitted — so a hand-written backlog is preserved. On a fresh project the file is created (empty `[]` if you skip the first-task prompt). If the file contains invalid JSON, init leaves it untouched and warns you to fix it and run `ahk sync`.
|
|
798
|
+
|
|
767
799
|
```json
|
|
768
800
|
[
|
|
769
801
|
{
|
|
@@ -799,8 +831,8 @@ The harness exposes these tools via MCP. Agents use them instead of reading file
|
|
|
799
831
|
| `actions.write` | `actionId, sectionType, content` | Record a text section: `result \| tools_used \| blockers \| next_steps`. Does **not** populate the Files dashboard — use `actions.record_file` for that |
|
|
800
832
|
| `actions.complete` | `actionId, summary` | Close an action with a one-line summary |
|
|
801
833
|
| `actions.get` | `taskId` | Full action history for a task (all agents, all sections) |
|
|
802
|
-
| `actions.record_file` | `actionId, filePath, operation, notes
|
|
803
|
-
| `actions.record_tool` | `actionId, toolName, argsJson?, resultSummary
|
|
834
|
+
| `actions.record_file` | `actionId, files: [{ filePath, operation, notes? }, ...]` | Batch-register one or more file touches, atomically. The **only** way to populate the Files dashboard. `operation`: `read \| created \| modified \| deleted`. Batch-only — `files` requires at least one entry; a single touch is still a one-element array |
|
|
835
|
+
| `actions.record_tool` | `actionId, calls: [{ toolName, argsJson?, resultSummary? }, ...]` | Batch-register one or more tool calls, atomically. The **only** way to populate the Tools dashboard. Batch-only — `calls` requires at least one entry; a single call is still a one-element array |
|
|
804
836
|
| `docs.search` | `query` | Search the `docsPath` folder for content matching the query |
|
|
805
837
|
| `tasks.acceptance_get` | `taskId` | Returns all acceptance criteria for a task with their `id`, `task_id`, `criterion` text, and `met` status. Use the returned `id` values with `tasks.acceptance.update` |
|
|
806
838
|
| `deps.snapshot` | _(none)_ | Snapshot current `package.json` dependencies to `.harness/deps-lock.json` |
|
|
@@ -32,36 +32,42 @@ a blocker and stop.
|
|
|
32
32
|
|
|
33
33
|
## !! MANDATORY TRACKING — DO THIS FOR EVERY ACTION, NO EXCEPTIONS !!
|
|
34
34
|
|
|
35
|
-
These
|
|
35
|
+
These calls are **not optional**. The dashboard cannot display what you do not report. Missing them is a failure of your role.
|
|
36
|
+
|
|
37
|
+
Both `actions.record_tool` and `actions.record_file` are **batch-only** — each takes an array of entries, never a single bespoke call. Accumulate as you work and flush periodically (every few tool calls, or at a natural checkpoint/phase boundary) rather than round-tripping once per individual tool use. Even a single entry must still go through the array shape — a one-element array, never a bespoke single-call form, since that form no longer exists.
|
|
36
38
|
|
|
37
39
|
### 1. Log every tool call you make
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Accumulate each tool invocation (Read, Edit, Write, Bash) as you go, then flush with:
|
|
40
42
|
|
|
41
43
|
```
|
|
42
|
-
actions.record_tool(actionId,
|
|
44
|
+
actions.record_tool(actionId, calls: [
|
|
45
|
+
{ toolName: '<ToolName>', argsJson: '<args-summary>', resultSummary: '<why>' },
|
|
46
|
+
...
|
|
47
|
+
])
|
|
43
48
|
```
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
- `actions.record_tool(actionId, 'Read', 'src/auth/middleware.ts', 'understand existing JWT pattern')`
|
|
47
|
-
- `actions.record_tool(actionId, 'Bash', 'npm test --testPathPattern=auth', 'verify auth tests pass')`
|
|
48
|
-
- `actions.record_tool(actionId, 'Edit', 'src/auth/middleware.ts:45-78', 'add refresh token validation')`
|
|
50
|
+
Example flush after a few calls:
|
|
51
|
+
- `actions.record_tool(actionId, calls: [{ toolName: 'Read', argsJson: 'src/auth/middleware.ts', resultSummary: 'understand existing JWT pattern' }, { toolName: 'Edit', argsJson: 'src/auth/middleware.ts:45-78', resultSummary: 'add refresh token validation' }, { toolName: 'Bash', argsJson: 'npm test --testPathPattern=auth', resultSummary: 'verify auth tests pass' }])`
|
|
49
52
|
|
|
50
53
|
### 2. Log every file you touch
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
Accumulate each file modification (Edit, Write) as you go, then flush with:
|
|
53
56
|
|
|
54
57
|
```
|
|
55
|
-
actions.record_file(actionId,
|
|
58
|
+
actions.record_file(actionId, files: [
|
|
59
|
+
{ filePath: '<file-path>', operation: '<operation>', notes: '<what changed and why>' },
|
|
60
|
+
...
|
|
61
|
+
])
|
|
56
62
|
```
|
|
57
63
|
|
|
58
64
|
Operations: `created` | `modified` | `deleted`
|
|
59
65
|
|
|
60
|
-
Example: `actions.record_file(actionId, 'src/auth/middleware.ts', 'modified', 'added refresh token expiry check in validateToken()')`
|
|
66
|
+
Example: `actions.record_file(actionId, files: [{ filePath: 'src/auth/middleware.ts', operation: 'modified', notes: 'added refresh token expiry check in validateToken()' }])`
|
|
61
67
|
|
|
62
68
|
### 3. Do not complete your action without both logs being up to date
|
|
63
69
|
|
|
64
|
-
If you touched 5 files and made 12 tool calls,
|
|
70
|
+
If you touched 5 files and made 12 tool calls across the session, every one of those must appear as an entry inside some `actions.record_file`/`actions.record_tool` batch call before you call `actions.complete` — it doesn't need to be 5 and 12 separate MCP round-trips, but the union of all your batched arrays must account for all 5 files and all 12 tool calls.
|
|
65
71
|
|
|
66
72
|
---
|
|
67
73
|
|
|
@@ -83,7 +89,7 @@ actions.start(taskId, 'builder') → save the returned actionId
|
|
|
83
89
|
|
|
84
90
|
### 3. Implement in small, verifiable steps
|
|
85
91
|
|
|
86
|
-
Work through the plan item by item.
|
|
92
|
+
Work through the plan item by item. Accumulate each tool call and each file touched as described in the **MANDATORY TRACKING** section above, and flush in batches as you go — do not wait until the very end of the session to record everything at once.
|
|
87
93
|
|
|
88
94
|
### 4. Follow existing patterns
|
|
89
95
|
|
|
@@ -167,8 +173,8 @@ Before writing a commit message, detect whether the repo already enforces a comm
|
|
|
167
173
|
|
|
168
174
|
- **Read the plan and analysis first.** Never implement cold.
|
|
169
175
|
- **Stay inside the project.** Never write outside the project root.
|
|
170
|
-
- **Log every file you touch.**
|
|
171
|
-
- **Log every tool call.**
|
|
176
|
+
- **Log every file you touch.** Accumulate entries and flush via `actions.record_file(actionId, files: [...])` periodically as you Edit/Write — batch-only, even one file goes through as a one-element array.
|
|
177
|
+
- **Log every tool call.** Accumulate entries and flush via `actions.record_tool(actionId, calls: [...])` periodically as you Read, Edit, Write, Bash — batch-only, even one call goes through as a one-element array.
|
|
172
178
|
- **Leave tests green.** If tests fail after your changes, fix them before completing.
|
|
173
179
|
- **Do not refactor beyond the task scope.** Implement what was asked, nothing more.
|
|
174
180
|
- **If blocked, say so.** Do not invent workarounds for unclear requirements.
|
|
@@ -34,18 +34,21 @@ These calls are **not optional**. The dashboard cannot display what you do not r
|
|
|
34
34
|
|
|
35
35
|
### Log every tool call you make
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
`actions.record_tool` is **batch-only** — it takes an array of calls, never a single bespoke call. Accumulate the tool invocations you make (Read, Bash, grep, docs.search) as you go, and flush them periodically — every few calls, or at a natural checkpoint like finishing a file or a research thread — via:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
actions.record_tool(actionId,
|
|
40
|
+
actions.record_tool(actionId, calls: [
|
|
41
|
+
{ toolName: '<ToolName>', argsJson: '<args-summary>', resultSummary: '<why>' },
|
|
42
|
+
...
|
|
43
|
+
])
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
- `actions.record_tool(actionId, 'Read', 'src/auth/middleware.ts', 'find existing JWT pattern')`
|
|
45
|
-
- `actions.record_tool(actionId, 'Bash', 'grep -r "refreshToken" src/', 'locate all refresh token usages')`
|
|
46
|
-
- `actions.record_tool(actionId, 'docs.search', 'authentication middleware', 'check project docs for auth guidance')`
|
|
46
|
+
Even a single tool call must go through this array shape — a one-element array, never a bespoke single-call form.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Example flush after a few calls:
|
|
49
|
+
- `actions.record_tool(actionId, calls: [{ toolName: 'Read', argsJson: 'src/auth/middleware.ts', resultSummary: 'find existing JWT pattern' }, { toolName: 'Bash', argsJson: 'grep -r "refreshToken" src/', resultSummary: 'locate all refresh token usages' }, { toolName: 'docs.search', argsJson: 'authentication middleware', resultSummary: 'check project docs for auth guidance' }])`
|
|
50
|
+
|
|
51
|
+
**Every tool call must be logged, eventually, in a batch.** No silent reads. The Tools dashboard is built entirely from these `actions.record_tool` calls — accumulate as you work and flush before completing, don't let entries pile up unflushed.
|
|
49
52
|
|
|
50
53
|
---
|
|
51
54
|
|
|
@@ -81,7 +84,7 @@ Do NOT read the entire codebase. Be targeted.
|
|
|
81
84
|
|
|
82
85
|
### 5. Log every tool call as you make it
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
Accumulate each invocation as described in the **MANDATORY TRACKING** section above and flush periodically in batches — don't wait until the very end to record everything at once.
|
|
85
88
|
|
|
86
89
|
### 6. Produce a structured analysis
|
|
87
90
|
|
|
@@ -95,18 +95,21 @@ These calls are **not optional**. The dashboard cannot display what you do not r
|
|
|
95
95
|
|
|
96
96
|
### Log every tool call you make
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
`actions.record_tool` is **batch-only** — it takes an array of calls, never a single bespoke call. As you work, accumulate the tool invocations you make (Bash, tasks.get, tasks.claim, actions.get) and flush them periodically — every few calls, or at a natural checkpoint — via:
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
actions.record_tool(actionId,
|
|
101
|
+
actions.record_tool(actionId, calls: [
|
|
102
|
+
{ toolName: '<ToolName>', argsJson: '<args-summary>', resultSummary: '<why/result>' },
|
|
103
|
+
...
|
|
104
|
+
])
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
- `actions.record_tool(actionId, 'Bash', 'bash health.sh', 'verify codebase health before making changes')`
|
|
106
|
-
- `actions.record_tool(actionId, 'tasks.get', 'pending', 'find next task to claim')`
|
|
107
|
-
- `actions.record_tool(actionId, 'actions.get', 'taskId=abc123', 'read action history to resume in-progress task')`
|
|
107
|
+
Even a single tool call must go through this array shape — a one-element array, never a bespoke single-call form.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Example flush after a few calls:
|
|
110
|
+
- `actions.record_tool(actionId, calls: [{ toolName: 'Bash', argsJson: 'bash health.sh', resultSummary: 'verify codebase health before making changes' }, { toolName: 'tasks.get', argsJson: 'pending', resultSummary: 'find next task to claim' }, { toolName: 'actions.get', argsJson: 'taskId=123', resultSummary: 'read action history to resume in-progress task' }])`
|
|
111
|
+
|
|
112
|
+
**Log every call, batched.** This applies from the moment you have an `actionId` (after step 3 below) — flush at each phase boundary rather than round-tripping once per individual tool use, and never let calls go unrecorded by the time you complete the action.
|
|
110
113
|
|
|
111
114
|
---
|
|
112
115
|
|
|
@@ -27,15 +27,19 @@ These calls are **not optional**. The dashboard cannot display what you do not r
|
|
|
27
27
|
|
|
28
28
|
### 1. Log every tool call you make
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
`actions.record_tool` is **batch-only** — it takes an array of calls, never a single bespoke call. Accumulate each tool invocation (Read, Bash) as you go, and flush periodically — every few calls, or at a natural checkpoint — via:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
actions.record_tool(actionId,
|
|
33
|
+
actions.record_tool(actionId, calls: [
|
|
34
|
+
{ toolName: '<ToolName>', argsJson: '<args-summary>', resultSummary: '<why>' },
|
|
35
|
+
...
|
|
36
|
+
])
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
Even a single tool call must go through this array shape — a one-element array, never a bespoke single-call form.
|
|
40
|
+
|
|
41
|
+
Example flush after a few calls:
|
|
42
|
+
- `actions.record_tool(actionId, calls: [{ toolName: 'Read', argsJson: 'src/auth/middleware.ts', resultSummary: 'verify refresh token logic matches criterion 2' }, { toolName: 'Bash', argsJson: 'npm test --testPathPattern=auth', resultSummary: 'confirm all auth tests pass' }])`
|
|
39
43
|
|
|
40
44
|
### 2. Mark every acceptance criterion as you verify it
|
|
41
45
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/core/db.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
3
|
import { homedir } from "os";
|
|
5
4
|
import { dirname, join, resolve } from "path";
|
|
@@ -10,10 +9,13 @@ var ActionRepository = class {
|
|
|
10
9
|
this.driver = driver;
|
|
11
10
|
}
|
|
12
11
|
driver;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
/** Returns the new autoincrement id — mirrors TaskRepository.add(). Since
|
|
13
|
+
* task #73, `actions.id` is a driver-generated INTEGER, not an
|
|
14
|
+
* application-generated UUID, so callers no longer pass an id in. */
|
|
15
|
+
async create(taskId, agent, now) {
|
|
16
|
+
return this.driver.insert(
|
|
17
|
+
`INSERT INTO actions (task_id, agent, status, created_at) VALUES (?, ?, 'in_progress', ?)`,
|
|
18
|
+
[taskId, agent, now]
|
|
17
19
|
);
|
|
18
20
|
}
|
|
19
21
|
async complete(actionId, summary, now) {
|
|
@@ -359,7 +361,7 @@ var TaskRepository = class {
|
|
|
359
361
|
};
|
|
360
362
|
|
|
361
363
|
// src/core/db.ts
|
|
362
|
-
var AUTOINCREMENT_TABLES = ["tasks", "task_acceptance", "action_sections", "action_files", "action_tools"];
|
|
364
|
+
var AUTOINCREMENT_TABLES = ["tasks", "task_acceptance", "actions", "action_sections", "action_files", "action_tools"];
|
|
363
365
|
var TABLE_INSERT_ORDER = ["tasks", "task_acceptance", "actions", "action_sections", "action_files", "action_tools"];
|
|
364
366
|
var TABLE_DELETE_ORDER = [...TABLE_INSERT_ORDER].reverse();
|
|
365
367
|
var DEFAULT_SQLITE_PATH = ".harness/harness.db";
|
|
@@ -464,9 +466,8 @@ var HarnessDB = class {
|
|
|
464
466
|
}
|
|
465
467
|
// ─── Actions (public facade — delegates to ActionRepository) ──────────────
|
|
466
468
|
async startAction(taskId, agent) {
|
|
467
|
-
const id = randomUUID();
|
|
468
469
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
-
await this.actions.create(
|
|
470
|
+
const id = await this.actions.create(taskId, agent, now);
|
|
470
471
|
await this.regenerateCurrentMd();
|
|
471
472
|
return await this.actions.getById(id);
|
|
472
473
|
}
|
|
@@ -494,12 +495,34 @@ var HarnessDB = class {
|
|
|
494
495
|
async getActionSections(actionId) {
|
|
495
496
|
return this.actions.getSections(actionId);
|
|
496
497
|
}
|
|
497
|
-
|
|
498
|
-
|
|
498
|
+
/** Batch-only (task #74) — records N files in one atomic transaction. There
|
|
499
|
+
* is no single-entry variant; callers pass a one-element array to log a
|
|
500
|
+
* single file. Mirrors the driver.transaction() pattern from claimTask()
|
|
501
|
+
* above: a fresh ActionRepository is bound to the tx driver so every
|
|
502
|
+
* insert in the loop participates in the same transaction and any failure
|
|
503
|
+
* rolls back the whole batch. Returns the number of files recorded. */
|
|
504
|
+
async recordFiles(actionId, files) {
|
|
505
|
+
return this.driver.transaction(async (tx) => {
|
|
506
|
+
const txActions = new ActionRepository(tx);
|
|
507
|
+
for (const f of files) {
|
|
508
|
+
await txActions.addFile(actionId, f.filePath, f.operation, f.notes ?? null);
|
|
509
|
+
}
|
|
510
|
+
return files.length;
|
|
511
|
+
});
|
|
499
512
|
}
|
|
500
|
-
|
|
513
|
+
/** Batch-only (task #74) — records N tool calls in one atomic transaction.
|
|
514
|
+
* See recordFiles() above for the pattern; a one-element array is the
|
|
515
|
+
* only way to log a single tool call. Returns the number of calls
|
|
516
|
+
* recorded. */
|
|
517
|
+
async recordTools(actionId, calls) {
|
|
501
518
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
502
|
-
return this.
|
|
519
|
+
return this.driver.transaction(async (tx) => {
|
|
520
|
+
const txActions = new ActionRepository(tx);
|
|
521
|
+
for (const c of calls) {
|
|
522
|
+
await txActions.addTool(actionId, c.toolName, c.argsJson ?? null, c.resultSummary ?? null, now);
|
|
523
|
+
}
|
|
524
|
+
return calls.length;
|
|
525
|
+
});
|
|
503
526
|
}
|
|
504
527
|
async getFilesForTask(taskId) {
|
|
505
528
|
return this.actions.getFilesForTask(taskId);
|
|
@@ -696,6 +719,11 @@ async function resetAutoincrementSequences(tx, dbType) {
|
|
|
696
719
|
}
|
|
697
720
|
}
|
|
698
721
|
async function importFullExport(destDriver, data, destDbType, opts = { truncateFirst: false }) {
|
|
722
|
+
if (data.actions.some((a) => typeof a.id !== "number")) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
"This export was produced by an older version of agent-harness-kit (actions used text/UUID ids, pre-2.0) and cannot be imported into a database using the current integer-id actions schema. Re-exporting from the old build is the only way to fix this \u2014 importing this file as-is is not supported."
|
|
725
|
+
);
|
|
726
|
+
}
|
|
699
727
|
await destDriver.transaction(async (tx) => {
|
|
700
728
|
if (opts.truncateFirst) {
|
|
701
729
|
await truncateAllTables(tx);
|
|
@@ -779,13 +807,13 @@ async function openDB(config, cwd, homeDir = homedir()) {
|
|
|
779
807
|
const dbConfig = config.database;
|
|
780
808
|
let driver;
|
|
781
809
|
if (dbConfig.type === "postgres") {
|
|
782
|
-
const { PostgresDriver } = await import("./postgres-
|
|
810
|
+
const { PostgresDriver } = await import("./postgres-BB4GY4PN.js");
|
|
783
811
|
driver = new PostgresDriver(dbConfig);
|
|
784
812
|
} else if (dbConfig.type === "mysql") {
|
|
785
|
-
const { MySQLDriver } = await import("./mysql-
|
|
813
|
+
const { MySQLDriver } = await import("./mysql-AUPKARWA.js");
|
|
786
814
|
driver = new MySQLDriver(dbConfig);
|
|
787
815
|
} else {
|
|
788
|
-
const { SQLiteDriver } = await import("./sqlite-
|
|
816
|
+
const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
|
|
789
817
|
if (dbConfig.type !== "sqlite") {
|
|
790
818
|
throw new Error("Invalid database type");
|
|
791
819
|
}
|
|
@@ -823,6 +851,7 @@ export {
|
|
|
823
851
|
HarnessDB,
|
|
824
852
|
getRowCounts,
|
|
825
853
|
isEmptyDatabase,
|
|
854
|
+
resetAutoincrementSequences,
|
|
826
855
|
importFullExport,
|
|
827
856
|
resolveSqlitePathForScope,
|
|
828
857
|
resolveSqlitePath,
|
|
@@ -831,4 +860,4 @@ export {
|
|
|
831
860
|
readStorageStateFile,
|
|
832
861
|
openDB
|
|
833
862
|
};
|
|
834
|
-
//# sourceMappingURL=chunk-
|
|
863
|
+
//# sourceMappingURL=chunk-JTACLEGM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/db.ts","../src/core/repositories/ActionRepository.ts","../src/core/repositories/StatsRepository.ts","../src/core/repositories/TaskRepository.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join, resolve } from 'node:path'\n\nimport { ActionRepository } from './repositories/ActionRepository'\nimport { StatsRepository } from './repositories/StatsRepository'\nimport { TaskRepository } from './repositories/TaskRepository'\n\nimport type { DBDriver } from './drivers/types'\nimport type {\n ActionFileRow,\n ActionRow,\n ActionSectionRow,\n ActionToolRow,\n AgentName,\n HarnessConfig,\n StorageState,\n TaskAcceptanceRow,\n TaskRow,\n TaskStatus,\n} from '@/types'\n\n/** Full relational export of every table — used by `ahk migrate storage` and\n * `ahk export --json`. MUST include all 6 tables (tasks, task_acceptance,\n * actions, action_sections, action_files, action_tools); omitting any of\n * them silently drops user data during a migration. */\nexport interface FullExport {\n tasks: TaskRow[]\n taskAcceptance: TaskAcceptanceRow[]\n actions: ActionRow[]\n sections: ActionSectionRow[]\n actionFiles: ActionFileRow[]\n actionTools: ActionToolRow[]\n}\n\n/** Tables with an integer autoincrement/serial primary key, in FK-safe\n * insertion order (parents before children). Since task #73, `actions.id`\n * is a driver-generated autoincrement INTEGER (previously an\n * application-generated UUID/TEXT id, which is why it used to be excluded\n * from this list) — see src/core/drivers/migrate-actions.ts for the\n * one-time migration that upgrades a pre-existing DB in place. */\nconst AUTOINCREMENT_TABLES = ['tasks', 'task_acceptance', 'actions', 'action_sections', 'action_files', 'action_tools'] as const\n\n/** Full insertion order across all 6 tables, respecting FK constraints\n * (parent before child): tasks -> task_acceptance -> actions ->\n * action_sections/action_files/action_tools. */\nconst TABLE_INSERT_ORDER = ['tasks', 'task_acceptance', 'actions', 'action_sections', 'action_files', 'action_tools'] as const\n\n/** Reverse of TABLE_INSERT_ORDER — used to TRUNCATE a non-empty destination\n * safely (children before parents) when `--force` is used. */\nconst TABLE_DELETE_ORDER = [...TABLE_INSERT_ORDER].reverse()\n\n// ─── Global storage path resolution ────────────────────────────────────────\n\n/** Default relative sqlite path used whenever `LocalStorageConfig.sqlitePath`\n * is omitted, and as the fallback filename component for `scope: 'global'`\n * (which never reads a configured path at all — see resolveSqlitePath()).\n * Centralized here (task #56) to stop the literal '.harness/harness.db'\n * from drifting across config.ts/templates.ts/tests. */\nexport const DEFAULT_SQLITE_PATH = '.harness/harness.db'\n\n/** Default relative current.md fallback path, used as the fallback filename\n * component whenever a caller needs \"the local markdown path\" for a scope\n * that ISN'T `config.storage`'s own current scope — `markdownFallback.path`\n * only exists on `LocalStorageConfig`, so there's no field to read it from\n * when `config.storage.scope === 'global'`. See `defaultMarkdownPathForConfig`\n * usage in src/commands/migrate-storage.ts. */\nexport const DEFAULT_MARKDOWN_PATH = '.harness/current.md'\n\n/** Resolves the directory used for 'global' scope storage: ~/.harness/dbs/<projectId>/\n * Uses os.homedir() (not $HOME env var) for portability. Callers are\n * responsible for creating the directory (mkdirSync recursive) before use. */\nexport function resolveGlobalStorageDir(config: HarnessConfig, homeDir: string = homedir()): string {\n return join(homeDir, '.harness', 'dbs', config.storage.projectId)\n}\n\n// ─── DB class ─────────────────────────────────────────────────────────────────\n\nexport class HarnessDB {\n readonly tasks: TaskRepository\n readonly actions: ActionRepository\n readonly stats: StatsRepository\n private driver: DBDriver\n private config: HarnessConfig\n /** Overridable home directory, used to keep 'global' scope tests off the real $HOME. */\n private homeDir: string\n\n constructor(driver: DBDriver, config: HarnessConfig, homeDir: string = homedir()) {\n this.driver = driver\n this.config = config\n this.homeDir = homeDir\n this.tasks = new TaskRepository(driver)\n this.actions = new ActionRepository(driver)\n this.stats = new StatsRepository(driver)\n }\n\n // ─── Tasks (public facade — delegates to TaskRepository) ──────────────────\n\n async addTask(params: {\n slug: string\n title: string\n description?: string\n acceptance?: string[]\n }): Promise<TaskRow> {\n const taskId = await this.tasks.add({\n slug: params.slug,\n title: params.title,\n description: params.description,\n })\n if (params.acceptance?.length) {\n await this.tasks.addAcceptance(taskId, params.acceptance)\n }\n await this.regenerateCurrentMd()\n return (await this.tasks.getById(taskId))!\n }\n\n async getTasks(status?: TaskStatus, includeArchived = false): Promise<TaskRow[]> {\n return this.tasks.getAll(status, includeArchived)\n }\n\n async getTaskById(id: number): Promise<TaskRow | null> {\n return this.tasks.getById(id)\n }\n\n async getTaskBySlug(slug: string): Promise<TaskRow | null> {\n return this.tasks.getBySlug(slug)\n }\n\n async getTaskAcceptance(taskId: number): Promise<TaskAcceptanceRow[]> {\n return this.tasks.getAcceptance(taskId)\n }\n\n async updateTaskStatus(idOrSlug: number | string, status: TaskStatus): Promise<TaskRow> {\n const now = new Date().toISOString()\n const task =\n typeof idOrSlug === 'number'\n ? await this.tasks.getById(idOrSlug)\n : await this.tasks.getBySlug(idOrSlug)\n if (!task) throw new Error(`Task not found: ${idOrSlug}`)\n\n if (status === 'in_progress' && !task.started_at) {\n await this.tasks.setStatus(task.id, status, { started_at: now })\n } else if (status === 'done') {\n await this.tasks.setStatus(task.id, status, { completed_at: now })\n } else {\n await this.tasks.setStatus(task.id, status)\n }\n\n await this.regenerateCurrentMd()\n return (await this.tasks.getById(task.id))!\n }\n\n async claimTask(id: number, agent: string): Promise<TaskRow | null> {\n const now = new Date().toISOString()\n return this.driver.transaction(async (tx) => {\n // need to create a new TaskRepository instance bound to the transaction\n const txTasks = new TaskRepository(tx)\n const changed = await txTasks.claim(id, agent, now)\n if (!changed) return null\n const task = await txTasks.getById(id)\n if (!task || task.status !== 'in_progress' || task.assigned_to !== agent) return null\n await this.regenerateCurrentMd()\n return task\n })\n }\n\n async markAcceptanceMet(criterionId: number): Promise<void> {\n return this.tasks.markAcceptanceMet(criterionId)\n }\n\n async updateTask(id: number, params: { title?: string; description?: string | null; slug?: string }): Promise<TaskRow> {\n await this.tasks.update(id, params)\n await this.regenerateCurrentMd()\n return (await this.tasks.getById(id))!\n }\n\n async updateTaskAcceptance(taskId: number, criteria: string[]): Promise<void> {\n await this.tasks.replaceAcceptance(taskId, criteria)\n await this.regenerateCurrentMd()\n }\n\n async archiveTask(id: number): Promise<TaskRow> {\n await this.tasks.archive(id)\n await this.regenerateCurrentMd()\n return (await this.tasks.getById(id))!\n }\n\n async unarchiveTask(id: number): Promise<TaskRow> {\n await this.tasks.unarchive(id)\n await this.regenerateCurrentMd()\n return (await this.tasks.getById(id))!\n }\n\n async getArchivedTasks(): Promise<TaskRow[]> {\n return this.tasks.getArchived()\n }\n\n async getStatusSummary(): Promise<{ status: string; total: number }[]> {\n return this.tasks.getStatusSummary()\n }\n\n // ─── Actions (public facade — delegates to ActionRepository) ──────────────\n\n async startAction(taskId: number, agent: AgentName): Promise<ActionRow> {\n const now = new Date().toISOString()\n const id = await this.actions.create(taskId, agent, now)\n await this.regenerateCurrentMd()\n return (await this.actions.getById(id))!\n }\n\n async writeSection(actionId: number, sectionType: string, content: string): Promise<void> {\n const now = new Date().toISOString()\n await this.actions.addSection(actionId, sectionType, content, now)\n await this.regenerateCurrentMd()\n }\n\n async completeAction(actionId: number, summary: string): Promise<ActionRow> {\n const now = new Date().toISOString()\n await this.actions.complete(actionId, summary, now)\n await this.regenerateCurrentMd()\n return (await this.actions.getById(actionId))!\n }\n\n async closeOrphanedActions(taskId: number): Promise<number> {\n const now = new Date().toISOString()\n return this.actions.closeOrphaned(taskId, now)\n }\n\n async getAction(actionId: number): Promise<ActionRow | null> {\n return this.actions.getById(actionId)\n }\n\n async getActionsForTask(taskId: number): Promise<ActionRow[]> {\n return this.actions.getForTask(taskId)\n }\n\n async getActionSections(actionId: number): Promise<ActionSectionRow[]> {\n return this.actions.getSections(actionId)\n }\n\n /** Batch-only (task #74) — records N files in one atomic transaction. There\n * is no single-entry variant; callers pass a one-element array to log a\n * single file. Mirrors the driver.transaction() pattern from claimTask()\n * above: a fresh ActionRepository is bound to the tx driver so every\n * insert in the loop participates in the same transaction and any failure\n * rolls back the whole batch. Returns the number of files recorded. */\n async recordFiles(\n actionId: number,\n files: Array<{ filePath: string; operation: ActionFileRow['operation']; notes?: string }>,\n ): Promise<number> {\n return this.driver.transaction(async (tx) => {\n const txActions = new ActionRepository(tx)\n for (const f of files) {\n await txActions.addFile(actionId, f.filePath, f.operation, f.notes ?? null)\n }\n return files.length\n })\n }\n\n /** Batch-only (task #74) — records N tool calls in one atomic transaction.\n * See recordFiles() above for the pattern; a one-element array is the\n * only way to log a single tool call. Returns the number of calls\n * recorded. */\n async recordTools(\n actionId: number,\n calls: Array<{ toolName: string; argsJson?: string; resultSummary?: string }>,\n ): Promise<number> {\n const now = new Date().toISOString()\n return this.driver.transaction(async (tx) => {\n const txActions = new ActionRepository(tx)\n for (const c of calls) {\n await txActions.addTool(actionId, c.toolName, c.argsJson ?? null, c.resultSummary ?? null, now)\n }\n return calls.length\n })\n }\n\n async getFilesForTask(taskId: number): Promise<(ActionFileRow & { agent: AgentName })[]> {\n return this.actions.getFilesForTask(taskId)\n }\n\n async getTopTools(limit = 10): Promise<{ tool_name: string; uses: number }[]> {\n return this.actions.getTopTools(limit)\n }\n\n // ─── current.md fallback ──────────────────────────────────────────────────\n\n async regenerateCurrentMd(): Promise<void> {\n if (!this.config.storage.markdownFallback.enabled) return\n\n const mdPath =\n this.config.storage.scope === 'global'\n ? join(resolveGlobalStorageDir(this.config, this.homeDir), 'current.md')\n : resolve(this.config.storage.markdownFallback.path)\n mkdirSync(dirname(mdPath), { recursive: true })\n\n const inProgress = await this.tasks.getAll('in_progress')\n const now = new Date().toISOString()\n\n let md = `<!-- AUTO-GENERATED by agent-harness-kit — DO NOT EDIT MANUALLY -->\\n`\n md += `<!-- Last updated: ${now} -->\\n\\n`\n md += `# Current Session\\n\\n`\n\n if (inProgress.length === 0) {\n md += `## No tasks in progress\\n\\n`\n const pending = await this.tasks.getAll('pending')\n if (pending.length > 0) {\n md += `### Next pending tasks\\n`\n for (const t of pending.slice(0, 5)) {\n md += `- **#${t.id}** ${t.title} (\\`${t.slug}\\`)\\n`\n }\n }\n } else {\n for (const task of inProgress) {\n md += `## Active Task\\n`\n md += `- **ID:** ${task.id}\\n`\n md += `- **Slug:** ${task.slug}\\n`\n md += `- **Status:** ${task.status}\\n`\n md += `- **Started:** ${task.started_at ?? 'unknown'}\\n\\n`\n\n const taskActions = await this.actions.getForTask(task.id)\n if (taskActions.length > 0) {\n md += `## Actions this session\\n`\n md += `| Agent | Status | Summary | Started |\\n`\n md += `|----------|-------------|----------------------------------|-------------|\\n`\n for (const a of taskActions) {\n const started = a.created_at.slice(11, 16)\n const summary = (a.summary ?? '').slice(0, 34).padEnd(34)\n md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |\\n`\n }\n md += `\\n`\n }\n\n const acceptance = await this.tasks.getAcceptance(task.id)\n if (acceptance.length > 0) {\n md += `## Acceptance Criteria\\n`\n for (const a of acceptance) {\n md += `- [${a.met ? 'x' : ' '}] ${a.criterion}\\n`\n }\n md += `\\n`\n }\n }\n }\n\n writeFileSync(mdPath, md, 'utf8')\n }\n\n // ─── Raw query escape hatch ───────────────────────────────────────────────\n\n async queryRaw<T = Record<string, unknown>>(sql: string, ...params: unknown[]): Promise<T[]> {\n return this.driver.query<T>(sql, params)\n }\n\n // ─── Export helpers ───────────────────────────────────────────────────────\n\n /** Full relational export of ALL 6 tables (tasks, task_acceptance, actions,\n * action_sections, action_files, action_tools). Extended for task #47 —\n * the previous version (tasks/actions/sections only) silently dropped\n * acceptance criteria and file/tool records on export/migrate. */\n async exportJson(): Promise<FullExport> {\n return {\n tasks: await this.tasks.getAll(undefined, true),\n taskAcceptance: await this.tasks.getAllAcceptance(),\n actions: await this.actions.getAll(),\n sections: await this.actions.getAllSections(),\n actionFiles: await this.actions.getAllFiles(),\n actionTools: await this.actions.getAllTools(),\n }\n }\n\n /** Row counts for all 6 tables against THIS db's driver — used to decide\n * whether a destination is \"empty\" (safe to import into directly) before\n * a migration. Counts are queried directly (COUNT(*)), never inferred\n * from storage-state.json. */\n async getRowCounts(): Promise<Record<(typeof TABLE_INSERT_ORDER)[number], number>> {\n return getRowCounts(this.driver)\n }\n\n /** Imports a full export into THIS db's driver — see standalone\n * `importFullExport()` for the transactional/rollback/sequence-reset\n * guarantees. `dbType` must match `this.config.database.type`. */\n async importFullExport(data: FullExport, dbType: 'sqlite' | 'postgres' | 'mysql', opts?: { truncateFirst: boolean }): Promise<void> {\n return importFullExport(this.driver, data, dbType, opts)\n }\n\n async reconnect(): Promise<void> {\n await this.driver.reconnect()\n }\n\n async close(): Promise<void> {\n await this.driver.close()\n }\n\n // ─── feature_list.json sync ───────────────────────────────────────────────\n\n async syncFromFeatureList(\n seeds: { slug: string; title: string; description?: string; acceptance?: string[] }[],\n ): Promise<{ added: number; skipped: number }> {\n let added = 0\n let skipped = 0\n for (const t of seeds) {\n if (await this.tasks.getBySlug(t.slug)) {\n skipped++\n continue\n }\n await this.addTask(t)\n added++\n }\n return { added, skipped }\n }\n\n async writeFeatureList(cwd: string): Promise<void> {\n const allTasks = await this.tasks.getAll(undefined, true)\n const list = await Promise.all(\n allTasks.map(async (t) => ({\n slug: t.slug,\n title: t.title,\n description: t.description ?? undefined,\n acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),\n status: t.status,\n })),\n )\n const path = join(resolve(cwd), this.config.storage.dir, 'feature_list.json')\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, JSON.stringify(list, null, 2) + '\\n', 'utf8')\n }\n\n // ─── storage-state.json (real storage state, for `ahk migrate storage`) ──\n\n /** Writes .harness/storage-state.json, ALWAYS project-local regardless of\n * scope. Reflects the REAL current storage state (scope/projectId/dbType\n * actually in use right now), as opposed to agent-harness-kit.config.ts\n * which reflects the DESIRED state. Format is stable — task #47 (ahk\n * migrate storage) depends on it; do not change field names/shape. */\n async writeStorageState(cwd: string): Promise<void> {\n writeStorageStateFile(cwd, this.config.storage.dir, {\n scope: this.config.storage.scope,\n projectId: this.config.storage.projectId,\n dbType: this.config.database.type,\n migratedAt: new Date().toISOString(),\n })\n }\n}\n\n// ─── Full DB migration helpers (task #47 — `ahk migrate storage`) ─────────\n\n/** Row counts for all 6 tables, queried directly (never inferred). Used to\n * decide whether a destination DB is \"empty\" before an sqlite↔remote\n * migration. */\nexport async function getRowCounts(driver: DBDriver): Promise<Record<(typeof TABLE_INSERT_ORDER)[number], number>> {\n const counts = {} as Record<(typeof TABLE_INSERT_ORDER)[number], number>\n for (const table of TABLE_INSERT_ORDER) {\n const row = await driver.queryOne<{ n: number }>(`SELECT COUNT(*) as n FROM ${table}`)\n counts[table] = Number(row?.n ?? 0)\n }\n return counts\n}\n\n/** True if every table is empty (COUNT(*) = 0 for all 6 tables). */\nexport async function isEmptyDatabase(driver: DBDriver): Promise<boolean> {\n const counts = await getRowCounts(driver)\n return Object.values(counts).every((n) => n === 0)\n}\n\n/** Deletes all rows from all 6 tables, children-before-parents, so FK\n * constraints never block the delete. Only ever called immediately before\n * a `--force` import, inside the same transaction as the import itself —\n * never on its own. */\nasync function truncateAllTables(tx: DBDriver): Promise<void> {\n for (const table of TABLE_DELETE_ORDER) {\n await tx.exec(`DELETE FROM ${table}`)\n }\n}\n\n/** Re-synchronizes the destination's internal autoincrement/serial counter\n * with the highest id actually present, AFTER inserting rows with explicit\n * ids. Required because inserting explicit ids does NOT advance\n * Postgres SERIAL sequences or SQLite's `sqlite_sequence` table — without\n * this, the first unrelated `INSERT ... (no id)` after a migration (e.g.\n * `tasksRepository.add()`) would collide with an imported id.\n * MySQL AUTO_INCREMENT advances automatically on explicit-id inserts\n * greater than the current counter — no action needed there. */\nexport async function resetAutoincrementSequences(\n tx: DBDriver,\n dbType: 'sqlite' | 'postgres' | 'mysql',\n): Promise<void> {\n if (dbType === 'mysql') return // AUTO_INCREMENT self-advances on explicit-id insert — verified in tests.\n\n for (const table of AUTOINCREMENT_TABLES) {\n const row = await tx.queryOne<{ max: number | null }>(`SELECT MAX(id) as max FROM ${table}`)\n const max = row?.max\n if (!max) continue // table stayed empty — nothing to advance\n\n if (dbType === 'postgres') {\n await tx.execRaw(`SELECT setval(pg_get_serial_sequence('${table}','id'), ${max}, true)`)\n } else {\n // sqlite: sqlite_sequence only gets a row once a real AUTOINCREMENT\n // insert happens; explicit-id inserts bypass that, so upsert it.\n await tx.execRaw(\n `INSERT INTO sqlite_sequence (name, seq) SELECT '${table}', ${max} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = '${table}')`,\n )\n await tx.execRaw(`UPDATE sqlite_sequence SET seq = ${max} WHERE name = '${table}'`)\n }\n }\n}\n\n/** Imports a full export (all 6 tables) into `destDriver`, preserving\n * original ids (required to keep foreign keys intact — see task #47\n * consultant advisory). The ENTIRE import (and, when `truncateFirst` is\n * set, the pre-import wipe) runs inside a single `destDriver.transaction()`\n * call: if anything fails partway, the whole operation rolls back and the\n * destination is left exactly as it was found.\n *\n * Callers MUST only mark the migration successful (e.g. call\n * `db.writeStorageState()`) AFTER this promise resolves without throwing —\n * never inside the transaction callback, never in a `finally`. */\nexport async function importFullExport(\n destDriver: DBDriver,\n data: FullExport,\n destDbType: 'sqlite' | 'postgres' | 'mysql',\n opts: { truncateFirst: boolean } = { truncateFirst: false },\n): Promise<void> {\n // Task #73: actions.id moved from a UUID/TEXT id to an autoincrement\n // INTEGER. An export produced by a pre-2.0 build still carries string\n // action ids, which would otherwise fail deep inside the transaction below\n // with a raw, confusing driver error (a datatype mismatch on sqlite's\n // INTEGER PRIMARY KEY rowid alias, or a hard insert error on postgres/\n // mysql). Fail fast with a clear, actionable message instead.\n if (data.actions.some((a) => typeof (a as { id: unknown }).id !== 'number')) {\n throw new Error(\n 'This export was produced by an older version of agent-harness-kit (actions used text/UUID ids, pre-2.0) and ' +\n 'cannot be imported into a database using the current integer-id actions schema. Re-exporting from the old build ' +\n 'is the only way to fix this — importing this file as-is is not supported.',\n )\n }\n\n await destDriver.transaction(async (tx) => {\n if (opts.truncateFirst) {\n await truncateAllTables(tx)\n }\n\n for (const task of data.tasks) {\n await tx.exec(\n `INSERT INTO tasks (id, slug, title, description, status, assigned_to, created_at, started_at, completed_at, archived_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n task.id,\n task.slug,\n task.title,\n task.description,\n task.status,\n task.assigned_to,\n task.created_at,\n task.started_at,\n task.completed_at,\n task.archived_at,\n task.updated_at,\n ],\n )\n }\n\n for (const ta of data.taskAcceptance) {\n await tx.exec(\n `INSERT INTO task_acceptance (id, task_id, criterion, met) VALUES (?, ?, ?, ?)`,\n [ta.id, ta.task_id, ta.criterion, ta.met],\n )\n }\n\n for (const action of data.actions) {\n await tx.exec(\n `INSERT INTO actions (id, task_id, agent, status, created_at, completed_at, summary) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [action.id, action.task_id, action.agent, action.status, action.created_at, action.completed_at, action.summary],\n )\n }\n\n for (const section of data.sections) {\n await tx.exec(\n `INSERT INTO action_sections (id, action_id, section_type, content, created_at) VALUES (?, ?, ?, ?, ?)`,\n [section.id, section.action_id, section.section_type, section.content, section.created_at],\n )\n }\n\n for (const file of data.actionFiles) {\n await tx.exec(\n `INSERT INTO action_files (id, action_id, file_path, operation, notes) VALUES (?, ?, ?, ?, ?)`,\n [file.id, file.action_id, file.file_path, file.operation, file.notes],\n )\n }\n\n for (const tool of data.actionTools) {\n await tx.exec(\n `INSERT INTO action_tools (id, action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?, ?)`,\n [tool.id, tool.action_id, tool.tool_name, tool.args_json, tool.result_summary, tool.called_at],\n )\n }\n\n await resetAutoincrementSequences(tx, destDbType)\n })\n}\n\n/** Resolves the physical sqlite file path for a given scope, sharing the\n * exact convention `openDB()` uses — extracted so `ahk migrate storage` can\n * locate the OLD file (by scope) without duplicating path logic. Does not\n * create any directories or files. */\nexport function resolveSqlitePathForScope(\n scope: 'local' | 'global',\n sqlitePath: string,\n cwd: string,\n config: HarnessConfig,\n homeDir: string,\n): string {\n return scope === 'global'\n ? join(resolveGlobalStorageDir(config, homeDir), 'harness.db')\n : resolve(cwd, sqlitePath)\n}\n\n/** Resolves the physical sqlite file path for `config`'s OWN current scope\n * (as opposed to `resolveSqlitePathForScope`, which resolves an arbitrary\n * scope — used by `ahk migrate storage` to probe both candidates). This is\n * the single mandatory entry point every call site should use instead of\n * reading `config.database`/`config.storage.sqlitePath` directly — routing\n * everything through here is what keeps new call sites from re-introducing\n * the \"reads a local-only field while scope=global\" bug class (task #55/#56). */\nexport function resolveSqlitePath(config: HarnessConfig, cwd: string, homeDir: string = homedir()): string {\n const sqlitePath = config.storage.scope === 'local' ? (config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH) : DEFAULT_SQLITE_PATH\n return resolveSqlitePathForScope(config.storage.scope, sqlitePath, cwd, config, homeDir)\n}\n\n/** Resolves the physical current.md fallback path for `config`'s OWN current\n * scope, sharing the exact convention `HarnessDB.regenerateCurrentMd()`\n * uses. Mirrors `resolveSqlitePath()` — call sites (materializers, reset,\n * health) should use this instead of reading `storage.markdownFallback.path`\n * directly, since that field doesn't exist at all under scope='global'. */\nexport function resolveMarkdownFallbackPath(config: HarnessConfig, cwd: string, homeDir: string = homedir()): string {\n return config.storage.scope === 'global'\n ? join(resolveGlobalStorageDir(config, homeDir), 'current.md')\n : resolve(cwd, config.storage.markdownFallback.path)\n}\n\n/** Writes `<storageDir>/storage-state.json` under `cwd`. Standalone (not tied\n * to a live HarnessDB instance) so it can be called during init before a DB\n * connection exists, and reused by future migration tooling. */\nexport function writeStorageStateFile(cwd: string, storageDir: string, state: StorageState): void {\n const path = join(resolve(cwd), storageDir, 'storage-state.json')\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, JSON.stringify(state, null, 2) + '\\n', 'utf8')\n}\n\n/** Reads `<storageDir>/storage-state.json` under `cwd`. Returns `null` if it\n * doesn't exist or is malformed. Used by future migration tooling (#47) to\n * determine the real current storage state before migrating. */\nexport function readStorageStateFile(cwd: string, storageDir: string): StorageState | null {\n try {\n const path = join(resolve(cwd), storageDir, 'storage-state.json')\n if (!existsSync(path)) return null\n return JSON.parse(readFileSync(path, 'utf8')) as StorageState\n } catch {\n return null\n }\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\nexport async function openDB(config: HarnessConfig, cwd: string, homeDir: string = homedir()): Promise<HarnessDB> {\n const dbConfig = config.database\n let driver: DBDriver\n\n if (dbConfig.type === 'postgres') {\n const { PostgresDriver } = await import('./drivers/postgres')\n driver = new PostgresDriver(dbConfig)\n } else if (dbConfig.type === 'mysql') {\n const { MySQLDriver } = await import('./drivers/mysql')\n driver = new MySQLDriver(dbConfig)\n } else {\n const { SQLiteDriver } = await import('./drivers/sqlite')\n if (dbConfig.type !== 'sqlite') {\n throw new Error('Invalid database type')\n }\n\n let dbPath: string\n if (config.storage.scope === 'global') {\n const globalDir = resolveGlobalStorageDir(config, homeDir)\n // Defensive check: a UUID collision is negligible, but if the target\n // dir already exists with a DIFFERENT project's state, don't silently\n // reuse it — surface the conflict instead of assuming it's free.\n const existingStatePath = join(globalDir, 'storage-state.json')\n if (existsSync(existingStatePath)) {\n try {\n const existingState = JSON.parse(readFileSync(existingStatePath, 'utf8')) as StorageState\n if (existingState.projectId !== config.storage.projectId) {\n throw new Error(\n `Global storage dir ${globalDir} already holds a different project (projectId: ${existingState.projectId}). Refusing to reuse it.`,\n )\n }\n } catch (err) {\n if (err instanceof Error && err.message.includes('already holds a different project')) throw err\n // Malformed/unreadable state file — ignore and proceed, mkdirSync below is idempotent.\n }\n }\n mkdirSync(globalDir, { recursive: true })\n dbPath = join(globalDir, 'harness.db')\n } else {\n dbPath = resolve(cwd, config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH)\n }\n\n driver = new SQLiteDriver(dbPath)\n }\n\n await driver.ensureSchema()\n return new HarnessDB(driver, config, homeDir)\n}\n","import type { DBDriver } from '../drivers/types'\nimport type { ActionFileRow, ActionRow, ActionSectionRow, ActionToolRow, AgentName } from '@/types'\n\nexport interface ActionWithDetails extends ActionRow {\n sections: ActionSectionRow[]\n files: ActionFileRow[]\n tools: ActionToolRow[]\n}\n\nexport class ActionRepository {\n constructor(private driver: DBDriver) {}\n\n /** Returns the new autoincrement id — mirrors TaskRepository.add(). Since\n * task #73, `actions.id` is a driver-generated INTEGER, not an\n * application-generated UUID, so callers no longer pass an id in. */\n async create(taskId: number, agent: AgentName, now: string): Promise<number> {\n return this.driver.insert(\n `INSERT INTO actions (task_id, agent, status, created_at) VALUES (?, ?, 'in_progress', ?)`,\n [taskId, agent, now],\n )\n }\n\n async complete(actionId: number, summary: string, now: string): Promise<void> {\n await this.driver.exec(\n `UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,\n [now, summary, actionId],\n )\n }\n\n async closeOrphaned(taskId: number, now: string): Promise<number> {\n return this.driver.exec(\n `UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,\n [now, taskId],\n )\n }\n\n async getById(actionId: number): Promise<ActionRow | null> {\n return this.driver.queryOne<ActionRow>(`SELECT * FROM actions WHERE id = ?`, [actionId])\n }\n\n async getForTask(taskId: number): Promise<ActionRow[]> {\n return this.driver.query<ActionRow>(\n `SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,\n [taskId],\n )\n }\n\n async getAll(): Promise<ActionRow[]> {\n return this.driver.query<ActionRow>(`SELECT * FROM actions ORDER BY created_at`)\n }\n\n async getWithDetails(taskId: number): Promise<ActionWithDetails[]> {\n const actions = await this.getForTask(taskId)\n return Promise.all(\n actions.map(async (action) => ({\n ...action,\n sections: await this.getSections(action.id),\n files: await this.getFiles(action.id),\n tools: await this.getTools(action.id),\n })),\n )\n }\n\n // ─── Sections ─────────────────────────────────────────────────────────────\n\n async addSection(actionId: number, sectionType: string, content: string, now: string): Promise<void> {\n await this.driver.exec(\n `INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,\n [actionId, sectionType, content, now],\n )\n }\n\n async getSections(actionId: number): Promise<ActionSectionRow[]> {\n return this.driver.query<ActionSectionRow>(\n `SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,\n [actionId],\n )\n }\n\n async getAllSections(): Promise<ActionSectionRow[]> {\n return this.driver.query<ActionSectionRow>(`SELECT * FROM action_sections ORDER BY created_at`)\n }\n\n // ─── Files ────────────────────────────────────────────────────────────────\n\n async addFile(\n actionId: number,\n filePath: string,\n operation: ActionFileRow['operation'],\n notes: string | null,\n ): Promise<void> {\n await this.driver.exec(\n `INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,\n [actionId, filePath, operation, notes],\n )\n }\n\n async getFiles(actionId: number): Promise<ActionFileRow[]> {\n return this.driver.query<ActionFileRow>(\n `SELECT * FROM action_files WHERE action_id = ?`,\n [actionId],\n )\n }\n\n async getFilesForTask(taskId: number): Promise<(ActionFileRow & { agent: AgentName })[]> {\n return this.driver.query<ActionFileRow & { agent: AgentName }>(\n `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`,\n [taskId],\n )\n }\n\n /** Returns ALL action_files rows regardless of action — used by full DB\n * exports (e.g. `ahk migrate storage`) so file-touch records aren't lost. */\n async getAllFiles(): Promise<ActionFileRow[]> {\n return this.driver.query<ActionFileRow>(`SELECT * FROM action_files ORDER BY id`)\n }\n\n // ─── Tools ────────────────────────────────────────────────────────────────\n\n async addTool(\n actionId: number,\n toolName: string,\n argsJson: string | null,\n resultSummary: string | null,\n now: string,\n ): Promise<void> {\n await this.driver.exec(\n `INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,\n [actionId, toolName, argsJson, resultSummary, now],\n )\n }\n\n async getTools(actionId: number): Promise<ActionToolRow[]> {\n return this.driver.query<ActionToolRow>(\n `SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,\n [actionId],\n )\n }\n\n /** Returns ALL action_tools rows regardless of action — used by full DB\n * exports (e.g. `ahk migrate storage`) so tool-call records aren't lost. */\n async getAllTools(): Promise<ActionToolRow[]> {\n return this.driver.query<ActionToolRow>(`SELECT * FROM action_tools ORDER BY id`)\n }\n\n async getTopTools(limit: number): Promise<{ tool_name: string; uses: number }[]> {\n return this.driver.query<{ tool_name: string; uses: number }>(\n `SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,\n [limit],\n )\n }\n}\n","import type { DBDriver } from '../drivers/types'\nimport type {\n AgentStatRow,\n CountRow,\n RecentFileRow,\n RecentToolRow,\n TimelineRow,\n TopFileRow,\n} from '../server-types'\n\nexport interface DBCounts {\n totalActions: number\n totalFiles: number\n uniqueTools: number\n activeAgents: number\n}\n\nexport { AgentStatRow, RecentFileRow, RecentToolRow, TimelineRow, TopFileRow }\n\nconst AGENT_ORDER = ['lead', 'explorer', 'builder', 'reviewer']\n\nexport class StatsRepository {\n constructor(private driver: DBDriver) { }\n\n async getCounts(): Promise<DBCounts> {\n const [{ total: totalActions }] = await this.driver.query<CountRow>(\n `SELECT COUNT(*) as total FROM actions`,\n )\n const [{ total: totalFiles }] = await this.driver.query<CountRow>(\n `SELECT COUNT(*) as total FROM action_files`,\n )\n const [{ total: uniqueTools }] = await this.driver.query<CountRow>(\n `SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`,\n )\n const [{ total: activeAgents }] = await this.driver.query<CountRow>(\n `SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`,\n )\n return { totalActions, totalFiles, uniqueTools, activeAgents }\n }\n\n async getRecentTools(limit: number): Promise<RecentToolRow[]> {\n return this.driver.query<RecentToolRow>(\n `SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent\n FROM action_tools at\n JOIN actions a ON at.action_id = a.id\n JOIN tasks t ON a.task_id = t.id\n ORDER BY at.called_at DESC\n LIMIT ?`,\n [limit],\n )\n }\n\n async getTopFiles(limit: number): Promise<TopFileRow[]> {\n return this.driver.query<TopFileRow>(\n `SELECT\n file_path,\n COUNT(*) as total,\n SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,\n SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,\n SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,\n SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted\n FROM action_files\n GROUP BY file_path\n ORDER BY total DESC\n LIMIT ?`,\n [limit],\n )\n }\n\n async getRecentFiles(limit: number): Promise<RecentFileRow[]> {\n return this.driver.query<RecentFileRow>(\n `SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,\n a.agent, a.created_at as called_at\n FROM action_files af\n JOIN actions a ON af.action_id = a.id\n JOIN tasks t ON a.task_id = t.id\n ORDER BY a.created_at DESC\n LIMIT ?`,\n [limit],\n )\n }\n\n async getAgentStats(): Promise<AgentStatRow[]> {\n const rows = await this.driver.query<AgentStatRow>(\n `SELECT\n a.agent,\n COUNT(*) as actions_total,\n SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,\n SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,\n COUNT(DISTINCT a.task_id) as tasks_worked,\n COUNT(DISTINCT af.file_path) as files_touched\n FROM actions a\n LEFT JOIN action_files af ON af.action_id = a.id\n GROUP BY a.agent\n ORDER BY actions_total DESC`,\n )\n return rows.sort((a, b) => {\n const ai = AGENT_ORDER.indexOf(a.agent)\n const bi = AGENT_ORDER.indexOf(b.agent)\n if (ai === -1 && bi === -1) return 0\n if (ai === -1) return 1\n if (bi === -1) return -1\n return ai - bi\n })\n }\n\n async getTimeline(limit: number): Promise<TimelineRow[]> {\n return this.driver.query<TimelineRow>(\n `SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status\n FROM actions a\n JOIN tasks t ON a.task_id = t.id\n ORDER BY a.created_at DESC\n LIMIT ?`,\n [limit],\n )\n }\n}\n","import type { DBDriver } from '../drivers/types'\nimport type { TaskAcceptanceRow, TaskRow, TaskStatus } from '@/types'\n\nexport interface TaskWithAcceptance extends TaskRow {\n acceptance_total: number\n acceptance_met: number\n}\n\nexport class TaskRepository {\n constructor(private driver: DBDriver) {}\n\n async add(params: {\n slug: string\n title: string\n description?: string | null\n status?: TaskStatus\n }): Promise<number> {\n const now = new Date().toISOString()\n return this.driver.insert(\n `INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n [params.slug, params.title, params.description ?? null, params.status ?? 'pending', now, now],\n )\n }\n\n async addAcceptance(taskId: number, criteria: string[]): Promise<void> {\n for (const criterion of criteria) {\n await this.driver.exec(\n `INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,\n [taskId, criterion],\n )\n }\n }\n\n async getAll(status?: TaskStatus, includeArchived = false): Promise<TaskRow[]> {\n let sql = `SELECT * FROM tasks`\n const params: unknown[] = []\n const conditions: string[] = []\n\n if (!includeArchived) {\n conditions.push(`archived_at IS NULL`)\n }\n if (status) {\n conditions.push(`status = ?`)\n params.push(status)\n }\n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`\n }\n sql += ` ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, updated_at DESC`\n\n return this.driver.query<TaskRow>(sql, params)\n }\n\n async getAllWithAcceptanceCounts(includeArchived = false): Promise<TaskWithAcceptance[]> {\n let sql = `\n SELECT t.*,\n COUNT(ta.id) as acceptance_total,\n COALESCE(SUM(ta.met), 0) as acceptance_met\n FROM tasks t\n LEFT JOIN task_acceptance ta ON ta.task_id = t.id\n `\n if (!includeArchived) {\n sql += ` WHERE t.archived_at IS NULL`\n }\n sql += ` GROUP BY t.id ORDER BY CASE t.status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, t.updated_at DESC`\n return this.driver.query<TaskWithAcceptance>(sql)\n }\n\n async getById(id: number): Promise<TaskRow | null> {\n return this.driver.queryOne<TaskRow>(`SELECT * FROM tasks WHERE id = ?`, [id])\n }\n\n async getBySlug(slug: string): Promise<TaskRow | null> {\n return this.driver.queryOne<TaskRow>(`SELECT * FROM tasks WHERE slug = ?`, [slug])\n }\n\n async getAcceptance(taskId: number): Promise<TaskAcceptanceRow[]> {\n return this.driver.query<TaskAcceptanceRow>(\n `SELECT * FROM task_acceptance WHERE task_id = ?`,\n [taskId],\n )\n }\n\n /** Returns ALL task_acceptance rows regardless of task — used by full DB\n * exports (e.g. `ahk migrate storage`) so criteria aren't silently dropped. */\n async getAllAcceptance(): Promise<TaskAcceptanceRow[]> {\n return this.driver.query<TaskAcceptanceRow>(`SELECT * FROM task_acceptance ORDER BY id`)\n }\n\n async setStatus(id: number, status: TaskStatus, extra?: { started_at?: string; completed_at?: string }): Promise<void> {\n const now = new Date().toISOString()\n if (extra?.started_at) {\n await this.driver.exec(\n `UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,\n [status, extra.started_at, now, id],\n )\n } else if (extra?.completed_at) {\n await this.driver.exec(\n `UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,\n [status, extra.completed_at, now, id],\n )\n } else {\n await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id])\n }\n }\n\n async update(id: number, params: { title?: string; description?: string | null; slug?: string }): Promise<void> {\n const sets: string[] = []\n const vals: unknown[] = []\n const now = new Date().toISOString()\n if (params.title !== undefined) { sets.push('title = ?'); vals.push(params.title) }\n if (params.description !== undefined) { sets.push('description = ?'); vals.push(params.description) }\n if (params.slug !== undefined) { sets.push('slug = ?'); vals.push(params.slug) }\n if (sets.length === 0) return\n sets.push('updated_at = ?')\n vals.push(now)\n vals.push(id)\n await this.driver.exec(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`, vals)\n }\n\n async replaceAcceptance(taskId: number, criteria: string[]): Promise<void> {\n await this.driver.exec(`DELETE FROM task_acceptance WHERE task_id = ?`, [taskId])\n for (const criterion of criteria) {\n await this.driver.exec(\n `INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,\n [taskId, criterion],\n )\n }\n }\n\n async archive(id: number): Promise<void> {\n const now = new Date().toISOString()\n await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id])\n }\n\n async unarchive(id: number): Promise<void> {\n const now = new Date().toISOString()\n await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id])\n }\n\n async getArchived(): Promise<TaskRow[]> {\n return this.driver.query<TaskRow>(\n `SELECT * FROM tasks WHERE archived_at IS NOT NULL ORDER BY archived_at DESC`\n )\n }\n\n async claim(id: number, agent: string, now: string): Promise<number> {\n return this.driver.exec(\n `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,\n [agent, now, now, id],\n )\n }\n\n async markAcceptanceMet(criterionId: number): Promise<void> {\n await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId])\n }\n\n async getStatusSummary(): Promise<{ status: string; total: number }[]> {\n return this.driver.query<{ status: string; total: number }>(\n `SELECT status, COUNT(*) as total FROM tasks WHERE archived_at IS NULL GROUP BY status`,\n )\n }\n}\n"],"mappings":"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","names":[]}
|