vibeman 0.0.11 → 0.0.13

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.
Files changed (38) hide show
  1. package/dist/api.js +53044 -34965
  2. package/dist/commit.txt +1 -1
  3. package/dist/index.js +166 -53
  4. package/dist/prisma/migrations/20260221100333_add_health_metrics/migration.sql +9 -0
  5. package/dist/prisma/schema.prisma +6 -0
  6. package/dist/prisma.config.ts +5 -1
  7. package/dist/scripts/init-test-repo.mjs +0 -8
  8. package/dist/scripts/lib/test-fixtures.mjs +67 -16
  9. package/dist/scripts/seed-test-fixtures.mjs +2 -2
  10. package/dist/ui/assets/{index-D4lRQ9OU.js → index-AacKgcwz.js} +1 -1
  11. package/dist/ui/assets/{index-Ck0eDlqj.js → index-B-SoOQ3F.js} +1 -1
  12. package/dist/ui/assets/{index-Buod5MG9.js → index-BCRvuZjU.js} +1 -1
  13. package/dist/ui/assets/{index-BtgmEMNX.js → index-BK62XAzY.js} +1 -1
  14. package/dist/ui/assets/{index-BKcn2ir8.js → index-B_lr0R8Z.js} +1 -1
  15. package/dist/ui/assets/{index-Q46jjFaN.js → index-BnaWildZ.js} +1 -1
  16. package/dist/ui/assets/{index-BFpKdhc4.js → index-BrOS5hPg.js} +1 -1
  17. package/dist/ui/assets/{index-DCwTMEKA.js → index-Btdv65e_.js} +1 -1
  18. package/dist/ui/assets/{index-CoX8THvk.js → index-BvHGNf4Y.js} +1 -1
  19. package/dist/ui/assets/{index-CfiNAuWd.js → index-C-AZWfRD.js} +1 -1
  20. package/dist/ui/assets/index-C8FDQRxy.js +1095 -0
  21. package/dist/ui/assets/{index-xr3-NPcF.js → index-CBQN5qXY.js} +1 -1
  22. package/dist/ui/assets/{index-BxFJT2l4.js → index-CWcSlUCR.js} +1 -1
  23. package/dist/ui/assets/{index-D_p2Z3lg.js → index-CXV7fZxu.js} +1 -1
  24. package/dist/ui/assets/{index-CJ0FVxY4.js → index-CuieGJbi.js} +1 -1
  25. package/dist/ui/assets/{index-DlPVzvxz.js → index-D1F9nIOz.js} +1 -1
  26. package/dist/ui/assets/{index-CmzQ8vUy.js → index-D3gXfrX-.js} +1 -1
  27. package/dist/ui/assets/{index-BnA5v3sz.js → index-DWT6BjYW.js} +1 -1
  28. package/dist/ui/assets/index-D_uhpUwJ.css +1 -0
  29. package/dist/ui/assets/{index-CCzed9cx.js → index-Dhu8BZu7.js} +1 -1
  30. package/dist/ui/assets/{index-XVbgp8h-.js → index-DkFfIHJr.js} +1 -1
  31. package/dist/ui/assets/{index-5kvaa7VH.js → index-cS068OSU.js} +1 -1
  32. package/dist/ui/assets/{index-CaM8gf-6.js → index-o8f2ilDp.js} +1 -1
  33. package/dist/ui/assets/{index-B2YlQpV0.js → index-q7X3WW0-.js} +1 -1
  34. package/dist/ui/index.html +2 -2
  35. package/package.json +7 -7
  36. package/dist/apps/api/resources/templates/task.md +0 -48
  37. package/dist/ui/assets/index-B-lQSV62.css +0 -1
  38. package/dist/ui/assets/index-BmCH7Zkp.js +0 -1020
package/dist/commit.txt CHANGED
@@ -1 +1 @@
1
- 07b258d
1
+ cf5eb36
package/dist/index.js CHANGED
@@ -1965,6 +1965,100 @@ function isNodeVersionCompatible(currentNodeVersion, requiredRange) {
1965
1965
  }
1966
1966
  }
1967
1967
 
1968
+ // ../shared/runtime/port-utils.ts
1969
+ import { createServer as createNetServer } from "node:net";
1970
+ var MIN_PORT = 1;
1971
+ var MAX_PORT = 65535;
1972
+ async function findAvailablePort(startPort) {
1973
+ assertValidPort(startPort);
1974
+ for (let port = startPort;port <= MAX_PORT; port += 1) {
1975
+ const available = await isPortAvailable(port);
1976
+ if (available) {
1977
+ return port;
1978
+ }
1979
+ }
1980
+ throw new Error(`No available port found from ${startPort} to ${MAX_PORT}.`);
1981
+ }
1982
+ function assertValidPort(port) {
1983
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
1984
+ throw new RangeError(`Port must be an integer in ${MIN_PORT}..${MAX_PORT}. Received: ${port}`);
1985
+ }
1986
+ }
1987
+ function isPortAvailable(port) {
1988
+ return new Promise((resolve2, reject) => {
1989
+ const probe = createNetServer();
1990
+ probe.unref();
1991
+ const onError = (error) => {
1992
+ cleanup();
1993
+ if (isAddressInUseError(error)) {
1994
+ resolve2(false);
1995
+ return;
1996
+ }
1997
+ reject(error);
1998
+ };
1999
+ const onListening = () => {
2000
+ cleanup();
2001
+ probe.close((closeError) => {
2002
+ if (closeError) {
2003
+ reject(closeError);
2004
+ return;
2005
+ }
2006
+ resolve2(true);
2007
+ });
2008
+ };
2009
+ const cleanup = () => {
2010
+ probe.off("error", onError);
2011
+ probe.off("listening", onListening);
2012
+ };
2013
+ probe.once("error", onError);
2014
+ probe.once("listening", onListening);
2015
+ probe.listen(port);
2016
+ });
2017
+ }
2018
+ function isAddressInUseError(error) {
2019
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2020
+ }
2021
+
2022
+ // ../shared/runtime/vibeman-env.ts
2023
+ var VIBEMAN_ENV_KEYS = {
2024
+ ROOT: "VIBEMAN_ROOT",
2025
+ PORT: "VIBEMAN_PORT",
2026
+ DATABASE_URL: "VIBEMAN_DATABASE_URL",
2027
+ VERBOSE: "VIBEMAN_VERBOSE",
2028
+ LOG_FORMAT: "VIBEMAN_LOG_FORMAT",
2029
+ UI_DIST: "VIBEMAN_UI_DIST",
2030
+ CODEX_PATH: "VIBEMAN_CODEX_PATH",
2031
+ GEMINI_PATH: "VIBEMAN_GEMINI_PATH",
2032
+ CLAUDE_CODE_PATH: "VIBEMAN_CLAUDE_CODE_PATH",
2033
+ TRACE: "VIBEMAN_TRACE",
2034
+ DIST_ROOT: "VIBEMAN_DIST_ROOT",
2035
+ DB_AUTORECOVER: "VIBEMAN_DB_AUTORECOVER"
2036
+ };
2037
+ function readVibemanEnv(env, key) {
2038
+ const value = env[key];
2039
+ if (typeof value === "string" && value.trim().length > 0) {
2040
+ return value.trim();
2041
+ }
2042
+ return;
2043
+ }
2044
+
2045
+ // src/dist-layout.ts
2046
+ var DIST_LAYOUT = {
2047
+ cliEntry: "index.js",
2048
+ apiEntry: "api.js",
2049
+ commitFile: "commit.txt",
2050
+ prismaConfig: "prisma.config.ts",
2051
+ uiRoot: "ui",
2052
+ uiIndexHtml: "ui/index.html",
2053
+ prismaRoot: "prisma",
2054
+ prismaSchema: "prisma/schema.prisma",
2055
+ resourcesRoot: "resources",
2056
+ scriptsRoot: "scripts",
2057
+ fixtureInitScript: "scripts/init-test-repo.mjs",
2058
+ fixtureSeedScript: "scripts/seed-test-fixtures.mjs",
2059
+ fixtureLibraryScript: "scripts/lib/test-fixtures.mjs"
2060
+ };
2061
+
1968
2062
  // src/index.ts
1969
2063
  enforceNodeRuntimeCompatibility(import.meta.url);
1970
2064
  var args = process.argv.slice(2);
@@ -1990,11 +2084,12 @@ console.error(`Unknown command: ${command}`);
1990
2084
  printHelp();
1991
2085
  process.exit(1);
1992
2086
  function parseStartArgs(argv) {
1993
- const envPort = Number(process.env.PORT);
2087
+ const envPort = Number(readVibemanEnv(process.env, VIBEMAN_ENV_KEYS.PORT));
1994
2088
  const output = {
1995
2089
  path: ".",
1996
2090
  port: Number.isFinite(envPort) ? envPort : 6969,
1997
- open: true
2091
+ open: true,
2092
+ trace: false
1998
2093
  };
1999
2094
  for (let i = 0;i < argv.length; i += 1) {
2000
2095
  const arg = argv[i];
@@ -2010,6 +2105,10 @@ function parseStartArgs(argv) {
2010
2105
  output.open = false;
2011
2106
  continue;
2012
2107
  }
2108
+ if (arg === "--trace") {
2109
+ output.trace = true;
2110
+ continue;
2111
+ }
2013
2112
  if (!arg.startsWith("-") && output.path === ".") {
2014
2113
  output.path = arg;
2015
2114
  }
@@ -2018,12 +2117,16 @@ function parseStartArgs(argv) {
2018
2117
  }
2019
2118
  async function startApps(options) {
2020
2119
  const moduleDir = dirname2(fileURLToPath2(import.meta.url));
2021
- const distRoot = resolveDistRoot(moduleDir);
2022
- const distApi = distRoot ? resolve2(distRoot, "api.js") : "";
2023
- const distUiRoot = distRoot ? resolve2(distRoot, "ui") : "";
2120
+ const distRoot = resolvePackagedDistRoot(moduleDir);
2121
+ const distApi = resolve2(distRoot, DIST_LAYOUT.apiEntry);
2122
+ const distUiRoot = resolve2(distRoot, DIST_LAYOUT.uiRoot);
2024
2123
  const nodePath = resolveNodePath();
2025
2124
  const targetRoot = resolve2(process.cwd(), options.path);
2026
- if (!distRoot || !existsSync2(distApi) || !existsSync2(resolve2(distUiRoot, "index.html"))) {
2125
+ const resolvedPort = await findAvailablePort(options.port);
2126
+ if (resolvedPort !== options.port) {
2127
+ console.warn(`[WARN] [startup] Requested port ${options.port} is in use. Using ${resolvedPort} instead.`);
2128
+ }
2129
+ if (!existsSync2(distApi) || !existsSync2(resolve2(distRoot, DIST_LAYOUT.uiIndexHtml))) {
2027
2130
  console.error("Missing dist runtime files. Run the build first.");
2028
2131
  process.exit(1);
2029
2132
  }
@@ -2031,22 +2134,28 @@ async function startApps(options) {
2031
2134
  cwd: process.cwd(),
2032
2135
  env: {
2033
2136
  ...process.env,
2034
- PORT: String(options.port),
2035
- VIBEMAN_ROOT: targetRoot,
2036
- UI_DIST: distUiRoot
2137
+ [VIBEMAN_ENV_KEYS.PORT]: String(resolvedPort),
2138
+ [VIBEMAN_ENV_KEYS.ROOT]: targetRoot,
2139
+ [VIBEMAN_ENV_KEYS.DIST_ROOT]: distRoot,
2140
+ [VIBEMAN_ENV_KEYS.UI_DIST]: distUiRoot,
2141
+ ...options.trace ? { [VIBEMAN_ENV_KEYS.VERBOSE]: "trace", [VIBEMAN_ENV_KEYS.TRACE]: "true" } : {}
2037
2142
  },
2038
2143
  stdio: "inherit"
2039
2144
  });
2040
- const url = new URL(`http://localhost:${options.port}/`);
2145
+ const url = new URL(`http://localhost:${resolvedPort}/`);
2041
2146
  if (options.open) {
2042
- const ready = await waitForApiReady(options.port);
2147
+ const startupSpinner = createStartupSpinner(url.toString(), resolvedPort);
2148
+ const ready = await waitForApiReady(resolvedPort);
2149
+ startupSpinner.stop(ready ? "ready" : "timeout");
2043
2150
  if (!ready) {
2044
2151
  console.warn("API not ready yet; opening the browser anyway.");
2045
2152
  }
2046
2153
  await openBrowser(url.toString());
2047
2154
  }
2048
- console.log(`
2049
- Vibeman running at ${url.toString()}. Press Ctrl+C to stop.`);
2155
+ const readyLine = process.stdout.isTTY ? `
2156
+ \x1B[32mReady\x1B[0m \x1B[96m${url.toString()}\x1B[0m (Ctrl+C to stop)` : `
2157
+ Ready ${url.toString()} (Ctrl+C to stop)`;
2158
+ console.log(readyLine);
2050
2159
  const stop = () => {
2051
2160
  apiProcess.kill();
2052
2161
  };
@@ -2080,16 +2189,12 @@ async function runFixtureScript(scriptName, forwardedArgs) {
2080
2189
  function resolveFixtureScriptPath(moduleDir, currentDir, scriptName) {
2081
2190
  const workspaceRoot = findFixtureWorkspaceRoot(moduleDir, currentDir);
2082
2191
  if (workspaceRoot) {
2083
- const scriptPath = resolve2(workspaceRoot, "scripts", scriptName);
2192
+ const scriptPath = resolve2(workspaceRoot, DIST_LAYOUT.scriptsRoot, scriptName);
2084
2193
  if (existsSync2(scriptPath)) {
2085
2194
  return scriptPath;
2086
2195
  }
2087
2196
  }
2088
- const packagedRoot = findPackagedFixtureRoot(moduleDir);
2089
- if (!packagedRoot) {
2090
- return "";
2091
- }
2092
- const packagedScript = resolve2(packagedRoot, "scripts", scriptName);
2197
+ const packagedScript = resolve2(resolvePackagedDistRoot(moduleDir), DIST_LAYOUT.scriptsRoot, scriptName);
2093
2198
  return existsSync2(packagedScript) ? packagedScript : "";
2094
2199
  }
2095
2200
  function findFixtureWorkspaceRoot(moduleDir, currentDir) {
@@ -2104,7 +2209,7 @@ function findFixtureWorkspaceRoot(moduleDir, currentDir) {
2104
2209
  function ascendForFixtureWorkspace(seed) {
2105
2210
  let dir = resolve2(seed);
2106
2211
  while (true) {
2107
- const fixtureScript = resolve2(dir, "scripts", "init-test-repo.mjs");
2212
+ const fixtureScript = resolve2(dir, DIST_LAYOUT.fixtureInitScript);
2108
2213
  const packageJson = resolve2(dir, "package.json");
2109
2214
  if (existsSync2(fixtureScript) && existsSync2(packageJson)) {
2110
2215
  return dir;
@@ -2115,21 +2220,6 @@ function ascendForFixtureWorkspace(seed) {
2115
2220
  dir = parent;
2116
2221
  }
2117
2222
  }
2118
- function findPackagedFixtureRoot(moduleDir) {
2119
- const candidates = [
2120
- moduleDir,
2121
- resolve2(moduleDir, ".."),
2122
- resolve2(moduleDir, "../dist"),
2123
- resolve2(moduleDir, "../../dist")
2124
- ];
2125
- for (const dir of candidates) {
2126
- const fixtureScript = resolve2(dir, "scripts", "init-test-repo.mjs");
2127
- if (existsSync2(fixtureScript)) {
2128
- return dir;
2129
- }
2130
- }
2131
- return "";
2132
- }
2133
2223
  async function openBrowser(target) {
2134
2224
  const platform = process.platform;
2135
2225
  if (platform === "darwin") {
@@ -2146,13 +2236,13 @@ function printHelp() {
2146
2236
  console.log(`vibeman CLI
2147
2237
 
2148
2238
  Usage:
2149
- vibeman start [path] [--port <port>] [--no-open]
2239
+ vibeman start [path] [--port <port>] [--no-open] [--trace]
2150
2240
  vibeman init-test-repo [path] [--force] [--skip-db-seed]
2151
2241
  vibeman --version
2152
2242
 
2153
2243
  Examples:
2154
2244
  vibeman start .
2155
- vibeman start ./notes --port 7010
2245
+ vibeman start ./notes --port 7010 --trace
2156
2246
  vibeman init-test-repo ./tmp/test-repo
2157
2247
  `);
2158
2248
  }
@@ -2185,7 +2275,7 @@ function readGitCommit() {
2185
2275
  function readPackagedCommit() {
2186
2276
  try {
2187
2277
  const moduleDir = dirname2(fileURLToPath2(import.meta.url));
2188
- const commitPath = resolve2(moduleDir, "commit.txt");
2278
+ const commitPath = resolve2(moduleDir, DIST_LAYOUT.commitFile);
2189
2279
  if (!existsSync2(commitPath))
2190
2280
  return "";
2191
2281
  return readFileSync2(commitPath, "utf-8").trim();
@@ -2198,6 +2288,42 @@ function waitForExit(child) {
2198
2288
  child.once("exit", (code) => resolvePromise(code));
2199
2289
  });
2200
2290
  }
2291
+ function createStartupSpinner(url, port) {
2292
+ if (!process.stdout.isTTY) {
2293
+ console.log(`[startup] Loading Vibeman... waiting for API on port ${port}`);
2294
+ return {
2295
+ stop: (result) => {
2296
+ if (result === "ready") {
2297
+ console.log(`[startup] Vibeman API is ready: ${url}`);
2298
+ return;
2299
+ }
2300
+ console.log(`[startup] Still loading after timeout; opening browser: ${url}`);
2301
+ }
2302
+ };
2303
+ }
2304
+ const frames = ["|", "/", "-", "\\"];
2305
+ let frameIndex = 0;
2306
+ const startMs = Date.now();
2307
+ process.stdout.write(`\r${frames[frameIndex]} Loading Vibeman... waiting for API on port ${port} `);
2308
+ const timer = setInterval(() => {
2309
+ frameIndex = (frameIndex + 1) % frames.length;
2310
+ const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
2311
+ process.stdout.write(`\r${frames[frameIndex]} Loading Vibeman... waiting for API on port ${port} (${elapsed}s) `);
2312
+ }, 120);
2313
+ return {
2314
+ stop: (result) => {
2315
+ clearInterval(timer);
2316
+ const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
2317
+ if (result === "ready") {
2318
+ process.stdout.write(`\r✓ Vibeman API ready in ${elapsed}s. Opening ${url}
2319
+ `);
2320
+ return;
2321
+ }
2322
+ process.stdout.write(`\r! Vibeman startup is taking longer than expected (${elapsed}s). Opening ${url}
2323
+ `);
2324
+ }
2325
+ };
2326
+ }
2201
2327
  async function waitForApiReady(port, timeoutMs = 30000, intervalMs = 250) {
2202
2328
  const startedAt = Date.now();
2203
2329
  while (Date.now() - startedAt < timeoutMs) {
@@ -2233,21 +2359,8 @@ function delay(ms) {
2233
2359
  setTimeout(resolvePromise, ms);
2234
2360
  });
2235
2361
  }
2236
- function resolveDistRoot(selfDir) {
2237
- const execDir = dirname2(process.execPath);
2238
- const candidates = [
2239
- selfDir,
2240
- resolve2(execDir, ".."),
2241
- resolve2(execDir, "../.."),
2242
- resolve2(selfDir, ".."),
2243
- resolve2(selfDir, "../..")
2244
- ];
2245
- for (const candidate of candidates) {
2246
- if (existsSync2(resolve2(candidate, "api.js")) && existsSync2(resolve2(candidate, "ui", "index.html"))) {
2247
- return candidate;
2248
- }
2249
- }
2250
- return "";
2362
+ function resolvePackagedDistRoot(moduleDir) {
2363
+ return moduleDir.endsWith("/dist") || moduleDir.endsWith("\\dist") ? moduleDir : resolve2(moduleDir, "..", "dist");
2251
2364
  }
2252
2365
  function resolveNodePath() {
2253
2366
  const execBase = basename(process.execPath);
@@ -0,0 +1,9 @@
1
+ -- AlterTable
2
+ ALTER TABLE "WorkflowRun" ADD COLUMN "completedAt" DATETIME;
3
+ ALTER TABLE "WorkflowRun" ADD COLUMN "durationMs" INTEGER;
4
+ ALTER TABLE "WorkflowRun" ADD COLUMN "startedAt" DATETIME;
5
+
6
+ -- AlterTable
7
+ ALTER TABLE "WorkflowRunStepResult" ADD COLUMN "completedAt" DATETIME;
8
+ ALTER TABLE "WorkflowRunStepResult" ADD COLUMN "durationMs" INTEGER;
9
+ ALTER TABLE "WorkflowRunStepResult" ADD COLUMN "startedAt" DATETIME;
@@ -67,6 +67,9 @@ model WorkflowRun {
67
67
  runPayload Json?
68
68
  initialRunPayload Json?
69
69
  lastError String?
70
+ startedAt DateTime?
71
+ completedAt DateTime?
72
+ durationMs Int?
70
73
  deletedAt DateTime?
71
74
  createdAt DateTime @default(now())
72
75
  updatedAt DateTime @updatedAt
@@ -95,6 +98,9 @@ model WorkflowRunStepResult {
95
98
  messages Json?
96
99
  graphState Json?
97
100
  tokenUsage Json?
101
+ startedAt DateTime?
102
+ completedAt DateTime?
103
+ durationMs Int?
98
104
  createdAt DateTime @default(now())
99
105
  updatedAt DateTime @updatedAt
100
106
 
@@ -1,8 +1,12 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { defineConfig } from 'prisma/config';
2
4
 
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
3
7
  export default defineConfig({
4
8
  schema: 'prisma/schema.prisma',
5
9
  datasource: {
6
- url: process.env.DATABASE_URL ?? 'file:./prisma/dev.db',
10
+ url: process.env.DATABASE_URL ?? `file:${resolve(__dirname, 'prisma/dev.db')}`,
7
11
  },
8
12
  });
@@ -11,7 +11,6 @@ import {
11
11
  TEST_FIXTURE_WORKFLOWS,
12
12
  buildTaskMarkdown,
13
13
  getRepoRoot,
14
- readTaskTemplate,
15
14
  taskFilename,
16
15
  taskRelativePath,
17
16
  } from './lib/test-fixtures.mjs';
@@ -104,12 +103,6 @@ async function ensureDirectoryIsEmpty(targetRoot, force) {
104
103
  }
105
104
  }
106
105
 
107
- async function writeTaskTemplate(targetRoot) {
108
- const templatePath = resolve(targetRoot, '.vibeman/templates/task.md');
109
- await mkdir(resolve(targetRoot, '.vibeman/templates'), { recursive: true });
110
- await writeFile(templatePath, readTaskTemplate(), 'utf8');
111
- }
112
-
113
106
  async function writeFixtureTaskFiles(targetRoot) {
114
107
  const tasksDir = resolve(targetRoot, '.vibeman/tasks');
115
108
  await mkdir(tasksDir, { recursive: true });
@@ -206,7 +199,6 @@ async function main() {
206
199
  const targetRoot = resolve(process.cwd(), args.path || '.');
207
200
 
208
201
  await ensureDirectoryIsEmpty(targetRoot, args.force);
209
- await writeTaskTemplate(targetRoot);
210
202
  await writeFixtureTaskFiles(targetRoot);
211
203
  await upsertGitignore(targetRoot);
212
204
  await ensureGitRepo(targetRoot);
@@ -1,9 +1,66 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
3
 
5
4
  const moduleDir = dirname(fileURLToPath(import.meta.url));
6
5
  const repoRoot = resolve(moduleDir, '..', '..');
6
+ const TASK_TEMPLATE = `---
7
+ # For human readability + AI parsing.
8
+ # For type/status/priority: values must match the options exactly and be lowercase.
9
+ id: [type]-[short-id]
10
+ title: [Short, descriptive title]
11
+ type: feature # feature, bug, chore, refactor, test, doc, other
12
+ status: backlog # backlog, in-progress, review, done
13
+ tags: [tag1, tag2]
14
+ priority: medium # low, medium, high
15
+ ---
16
+
17
+ ## Goal
18
+
19
+ [1-2 sentences: why this task exists and the end-user outcome. Do NOT list features here — save those for Requirements.]
20
+
21
+ ## Context
22
+
23
+ [Current state: how things work today, relevant code locations, links. State facts only — no actions or constraints.]
24
+
25
+ ## Requirements
26
+
27
+ [Each item = a verifiable condition that must be true when the task is done. Write as testable outcomes, not implementation actions. This is the single source of "what success looks like" — do not restate these in other sections.]
28
+
29
+ - [ ] Outcome #1
30
+ - [ ] Outcome #2
31
+
32
+ ## Implementation Notes
33
+
34
+ [Reference-only info for the implementer. No action verbs — use Recommended Steps for that.
35
+
36
+ - target files or directories
37
+ - patterns or conventions to follow
38
+ - commands to run for validation
39
+ - out-of-scope items or things not to change]
40
+
41
+ ## Recommended Steps
42
+
43
+ [The only place that describes HOW to do the work. Organize by implementation phase, not by requirement. Each step should advance multiple requirements at once when possible.]
44
+
45
+ - Step 1: [Phase name]
46
+ - [ ] Action item
47
+ - [ ] Action item
48
+ - Step 2: [Phase name]
49
+ - [ ] Action item
50
+
51
+ ## Open Questions
52
+
53
+ [Unresolved decisions or ambiguities. Use the structure below so responses are explicit and easy to fill in.]
54
+
55
+ - Question: [Open question #1?]
56
+ - Answer: [Pending]
57
+ - Question: [Open question #2?]
58
+ - Answer: [Pending]
59
+
60
+ ## Implementation Summary
61
+
62
+ [Auto-generated after completion: what changed + tests run.]
63
+ `;
7
64
 
8
65
  const DEFAULT_CODEX_MODEL = 'gpt-5.3-codex';
9
66
  const LOW_COST_GEMINI_MODEL = 'gemini-3-flash-preview';
@@ -812,7 +869,7 @@ export function taskRelativePath(taskId) {
812
869
  }
813
870
 
814
871
  export function readTaskTemplate() {
815
- return readFileSync(resolve(repoRoot, 'apps/api/resources/templates/task.md'), 'utf8');
872
+ return TASK_TEMPLATE;
816
873
  }
817
874
 
818
875
  function replaceOnce(input, from, to) {
@@ -867,31 +924,25 @@ export function buildTaskMarkdown(taskDef, nowIso = new Date().toISOString()) {
867
924
  `created_at: '${nowIso}'\nupdated_at: '${nowIso}'\n---\n\n## Goal`,
868
925
  );
869
926
 
927
+ task = replaceOnce(task, /\[1-2 sentences:[^\]]+\]/, taskDef.goal);
870
928
  task = replaceOnce(
871
929
  task,
872
- '[What outcome should exist when this is done? Keep it concrete and short.]',
873
- taskDef.goal,
874
- );
875
- task = replaceOnce(
876
- task,
877
- '[Key background, links, or constraints that matter to implementation.]',
930
+ /\[Current state:[^\]]+\]/,
878
931
  `${taskDef.context}\n\nTarget workflow: \`${taskDef.workflowId}\`.`,
879
932
  );
880
933
 
881
- task = replaceOnce(task, '- [ ] Primary requirement #1', `- [ ] ${requirementA}`);
882
- task = replaceOnce(task, '- [ ] Primary requirement #2', `- [ ] ${requirementB}`);
934
+ task = replaceOnce(task, '- [ ] Outcome #1', `- [ ] ${requirementA}`);
935
+ task = replaceOnce(task, '- [ ] Outcome #2', `- [ ] ${requirementB}`);
883
936
 
884
937
  task = replaceOnce(
885
938
  task,
886
- `[Notes for AI or human. Include only what matters:\n\n- target files or directories\n- desired approach or patterns to follow\n- tests/commands to run\n- any do-not-change or out-of-scope items]`,
939
+ /\[Reference-only info for the implementer\.[\s\S]+?not to change\]/,
887
940
  notes,
888
941
  );
889
942
 
890
- task = replaceOnce(task, '- [ ] Actionable TODO', `- [ ] ${stepA}`);
891
- task = replaceOnce(task, '- [ ] Actionable TODO', `- [ ] ${stepB}`);
892
-
893
- task = replaceOnce(task, '- [ ] Verifiable outcome #1', `- [ ] ${acceptanceA}`);
894
- task = replaceOnce(task, '- [ ] Verifiable outcome #2', `- [ ] ${acceptanceB}`);
943
+ task = replaceOnce(task, '- [ ] Action item', `- [ ] ${stepA}`);
944
+ task = replaceOnce(task, '- [ ] Action item', `- [ ] ${stepB}`);
945
+ task = `${task.trim()}\n\n## Acceptance Criteria\n\n- [ ] ${acceptanceA}\n- [ ] ${acceptanceB}\n`;
895
946
 
896
947
  return task;
897
948
  }
@@ -3,7 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { mkdir } from 'node:fs/promises';
5
5
  import { dirname, resolve } from 'node:path';
6
- import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
6
+ import { PrismaLibSql } from '@prisma/adapter-libsql';
7
7
 
8
8
  import {
9
9
  PRIMARY_TEST_WORKFLOW_ID,
@@ -214,7 +214,7 @@ async function main() {
214
214
  }
215
215
 
216
216
  const PrismaClient = await resolvePrismaClient();
217
- const adapter = new PrismaBetterSqlite3({ url: databaseUrl });
217
+ const adapter = new PrismaLibSql({ url: databaseUrl });
218
218
  const prisma = new PrismaClient({ adapter });
219
219
 
220
220
  try {