@hasna/workflows 0.1.2 → 0.1.3

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 CHANGED
@@ -102,7 +102,7 @@ console.log(svc.health());
102
102
  |---|---|---|
103
103
  | `HASNA_WORKFLOWS_PORT` / `WORKFLOWS_PORT` | serve port | 8790 |
104
104
  | `HASNA_WORKFLOWS_HOST` / `WORKFLOWS_HOST` | serve bind host | 127.0.0.1 |
105
- | `HASNA_WORKFLOWS_DATA_DIR` / `WORKFLOWS_DATA_DIR` | data directory | `~/.hasna/workflows` |
105
+ | `HASNA_WORKFLOWS_DATA_DIR` / `WORKFLOWS_DATA_DIR` | data directory | `~/.hasna/workflows` (the @hasna/paths XDG data home `~/.local/share/hasna/workflows` is adopted once `HASNA_DATA_HOME` is set or the store is migrated there) |
106
106
 
107
107
  The server data backend is sqlite (default) or postgresql when
108
108
  `HASNA_WORKFLOWS_DATABASE_URL` is set.
@@ -1411,9 +1411,9 @@ class WorkflowsDaemon {
1411
1411
  }
1412
1412
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1413
1413
  if (!result.ok) {
1414
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1415
- if (retriesLeft > 0) {
1416
- this.store.bumpAttemptsNode(nodeRow.id);
1414
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1415
+ this.store.bumpAttemptsNode(nodeRow.id);
1416
+ if (shouldRetry) {
1417
1417
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1418
1418
  return { kind: "advanced", nodeId };
1419
1419
  }
package/dist/cli/index.js CHANGED
@@ -443,16 +443,13 @@ var init_lanes = __esm(() => {
443
443
 
444
444
  // src/cli/index.ts
445
445
  import { Command } from "commander";
446
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync } from "fs";
446
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync } from "fs";
447
447
  import { hostname } from "os";
448
448
  import { join as join4 } from "path";
449
-
450
- // src/service.ts
451
- import { join as join3 } from "path";
452
449
  // package.json
453
450
  var package_default = {
454
451
  name: "@hasna/workflows",
455
- version: "0.1.2",
452
+ version: "0.1.3",
456
453
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
457
454
  type: "module",
458
455
  main: "./dist/index.js",
@@ -522,9 +519,10 @@ var package_default = {
522
519
  author: "Hasna",
523
520
  license: "Apache-2.0",
524
521
  dependencies: {
525
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
522
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
526
523
  "@cursor/sdk": "^1.0.28",
527
- "@openai/codex-sdk": "^0.147.0",
524
+ "@hasna/paths": "0.1.0",
525
+ "@openai/codex-sdk": "0.147.0",
528
526
  commander: "^13.1.0"
529
527
  },
530
528
  devDependencies: {
@@ -1535,9 +1533,9 @@ class WorkflowsDaemon {
1535
1533
  }
1536
1534
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1537
1535
  if (!result.ok) {
1538
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1539
- if (retriesLeft > 0) {
1540
- this.store.bumpAttemptsNode(nodeRow.id);
1536
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1537
+ this.store.bumpAttemptsNode(nodeRow.id);
1538
+ if (shouldRetry) {
1541
1539
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1542
1540
  return { kind: "advanced", nodeId };
1543
1541
  }
@@ -1669,17 +1667,44 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1669
1667
  return store.getRun(run.id);
1670
1668
  }
1671
1669
 
1670
+ // src/paths.ts
1671
+ import { existsSync } from "fs";
1672
+ import { homedir } from "os";
1673
+ import { join } from "path";
1674
+ import { dataDir } from "@hasna/paths";
1675
+ var APP = "workflows";
1676
+ var STORE_DB_FILE = "workflows.db";
1677
+ function getHomeDir(env = process.env) {
1678
+ return env["HOME"] || env["USERPROFILE"] || homedir();
1679
+ }
1680
+ function getLegacyDataDir(env = process.env) {
1681
+ return join(getHomeDir(env), ".hasna", APP);
1682
+ }
1683
+ function getResolverDataDir(env = process.env) {
1684
+ return dataDir({ app: APP, home: getHomeDir(env), env });
1685
+ }
1686
+ function adoptResolverDataDir(resolved, env = process.env) {
1687
+ const dataOverride = env.HASNA_DATA_HOME;
1688
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
1689
+ return true;
1690
+ return existsSync(join(resolved, STORE_DB_FILE));
1691
+ }
1692
+ function getEffectiveDataDir(env = process.env) {
1693
+ const resolved = getResolverDataDir(env);
1694
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
1695
+ }
1696
+
1672
1697
  // src/store.ts
1673
1698
  import { Database } from "bun:sqlite";
1674
1699
  import { mkdirSync } from "fs";
1675
- import { join } from "path";
1700
+ import { join as join2 } from "path";
1676
1701
  import { randomUUID } from "crypto";
1677
1702
  function nowIso2() {
1678
1703
  return new Date().toISOString();
1679
1704
  }
1680
- function openStore(dataDir) {
1681
- mkdirSync(dataDir, { recursive: true });
1682
- const db = new Database(join(dataDir, "workflows.db"));
1705
+ function openStore(dataDir2) {
1706
+ mkdirSync(dataDir2, { recursive: true });
1707
+ const db = new Database(join2(dataDir2, "workflows.db"));
1683
1708
  db.exec("PRAGMA journal_mode = WAL");
1684
1709
  db.exec("PRAGMA foreign_keys = ON");
1685
1710
  db.exec(`
@@ -1857,8 +1882,8 @@ function openStore(dataDir) {
1857
1882
 
1858
1883
  // src/wal.ts
1859
1884
  import { createHash as createHash3 } from "crypto";
1860
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1861
- import { join as join2 } from "path";
1885
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1886
+ import { join as join3 } from "path";
1862
1887
  var WAL_FILE_NAME = "session.wal";
1863
1888
  var SESSIONS_DIR_NAME = "sessions";
1864
1889
  var MAX_WAL_BYTES = 256 * 1024 * 1024;
@@ -1873,11 +1898,11 @@ class SessionWAL {
1873
1898
  constructor(filePath) {
1874
1899
  this.filePath = filePath;
1875
1900
  }
1876
- static open(dataDir) {
1877
- mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1878
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1879
- const legacy = join2(dataDir, WAL_FILE_NAME);
1880
- if (existsSync(legacy) && !existsSync(canonical)) {
1901
+ static open(dataDir2) {
1902
+ mkdirSync2(join3(dataDir2, SESSIONS_DIR_NAME), { recursive: true });
1903
+ const canonical = join3(dataDir2, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1904
+ const legacy = join3(dataDir2, WAL_FILE_NAME);
1905
+ if (existsSync2(legacy) && !existsSync2(canonical)) {
1881
1906
  try {
1882
1907
  renameSync(legacy, canonical);
1883
1908
  } catch {
@@ -2008,11 +2033,10 @@ function envInt(names, fallback) {
2008
2033
  return Number.isFinite(parsed) ? parsed : fallback;
2009
2034
  }
2010
2035
  function resolveWorkflowsConfig(overrides = {}) {
2011
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
2012
2036
  return {
2013
2037
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
2014
2038
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
2015
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2039
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
2016
2040
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
2017
2041
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
2018
2042
  };
@@ -2403,7 +2427,7 @@ program.command("init").description("Create the store layout (workflows/, sessio
2403
2427
  const store = openStore(config.dataDir);
2404
2428
  store.close();
2405
2429
  const target = file ?? join4(config.dataDir, "workflows", "demo.json");
2406
- if (existsSync2(target)) {
2430
+ if (existsSync3(target)) {
2407
2431
  console.error(`refusing to overwrite existing file ${target}`);
2408
2432
  process.exit(1);
2409
2433
  }
@@ -2690,16 +2714,16 @@ program.command("machines").description("List observed machines or report this m
2690
2714
  case "status": {
2691
2715
  const replay = wal.replay();
2692
2716
  const pidFile = join4(config.dataDir, "daemon.pid");
2693
- const pid = existsSync2(pidFile) ? readFileSync2(pidFile, "utf8").trim() : null;
2717
+ const pid = existsSync3(pidFile) ? readFileSync2(pidFile, "utf8").trim() : null;
2694
2718
  const running = pid !== null && processAlive(Number(pid));
2695
2719
  const report = {
2696
2720
  hostname: hostname(),
2697
2721
  pid: process.pid,
2698
2722
  dataDir: config.dataDir,
2699
2723
  layout: {
2700
- workflows: existsSync2(join4(config.dataDir, "workflows")),
2701
- sessions: existsSync2(join4(config.dataDir, SESSIONS_DIR_NAME)),
2702
- db: existsSync2(join4(config.dataDir, "workflows.db"))
2724
+ workflows: existsSync3(join4(config.dataDir, "workflows")),
2725
+ sessions: existsSync3(join4(config.dataDir, SESSIONS_DIR_NAME)),
2726
+ db: existsSync3(join4(config.dataDir, "workflows.db"))
2703
2727
  },
2704
2728
  store: {
2705
2729
  tables: store.listTables(),
@@ -2812,7 +2836,7 @@ program.command("daemon").description("Run or inspect the daemon").argument("<ac
2812
2836
  }
2813
2837
  }
2814
2838
  case "status": {
2815
- if (!existsSync2(statusFile)) {
2839
+ if (!existsSync3(statusFile)) {
2816
2840
  if (opts.json) {
2817
2841
  console.log(JSON.stringify({ running: false, lastReap: null }));
2818
2842
  } else {
@@ -2821,7 +2845,7 @@ program.command("daemon").description("Run or inspect the daemon").argument("<ac
2821
2845
  return;
2822
2846
  }
2823
2847
  const lastReap = JSON.parse(readFileSync2(statusFile, "utf8"));
2824
- const pid = existsSync2(pidFile) ? readFileSync2(pidFile, "utf8").trim() : null;
2848
+ const pid = existsSync3(pidFile) ? readFileSync2(pidFile, "utf8").trim() : null;
2825
2849
  const running = pid !== null && processAlive(Number(pid));
2826
2850
  if (opts.json) {
2827
2851
  console.log(JSON.stringify({ running, pid, lastReap }, null, 2));
@@ -2831,7 +2855,7 @@ program.command("daemon").description("Run or inspect the daemon").argument("<ac
2831
2855
  return;
2832
2856
  }
2833
2857
  case "stop": {
2834
- if (!existsSync2(pidFile)) {
2858
+ if (!existsSync3(pidFile)) {
2835
2859
  console.error("no daemon pid file \u2014 nothing to stop");
2836
2860
  process.exit(1);
2837
2861
  }
package/dist/index.js CHANGED
@@ -439,13 +439,10 @@ var init_lanes = __esm(() => {
439
439
  ["grok", createGrokAdapter()]
440
440
  ]);
441
441
  });
442
-
443
- // src/service.ts
444
- import { join as join3 } from "path";
445
442
  // package.json
446
443
  var package_default = {
447
444
  name: "@hasna/workflows",
448
- version: "0.1.2",
445
+ version: "0.1.3",
449
446
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
450
447
  type: "module",
451
448
  main: "./dist/index.js",
@@ -515,9 +512,10 @@ var package_default = {
515
512
  author: "Hasna",
516
513
  license: "Apache-2.0",
517
514
  dependencies: {
518
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
515
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
519
516
  "@cursor/sdk": "^1.0.28",
520
- "@openai/codex-sdk": "^0.147.0",
517
+ "@hasna/paths": "0.1.0",
518
+ "@openai/codex-sdk": "0.147.0",
521
519
  commander: "^13.1.0"
522
520
  },
523
521
  devDependencies: {
@@ -1500,9 +1498,9 @@ class WorkflowsDaemon {
1500
1498
  }
1501
1499
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1502
1500
  if (!result.ok) {
1503
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1504
- if (retriesLeft > 0) {
1505
- this.store.bumpAttemptsNode(nodeRow.id);
1501
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1502
+ this.store.bumpAttemptsNode(nodeRow.id);
1503
+ if (shouldRetry) {
1506
1504
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1507
1505
  return { kind: "advanced", nodeId };
1508
1506
  }
@@ -1634,17 +1632,44 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1634
1632
  return store.getRun(run.id);
1635
1633
  }
1636
1634
 
1635
+ // src/paths.ts
1636
+ import { existsSync } from "fs";
1637
+ import { homedir } from "os";
1638
+ import { join } from "path";
1639
+ import { dataDir } from "@hasna/paths";
1640
+ var APP = "workflows";
1641
+ var STORE_DB_FILE = "workflows.db";
1642
+ function getHomeDir(env = process.env) {
1643
+ return env["HOME"] || env["USERPROFILE"] || homedir();
1644
+ }
1645
+ function getLegacyDataDir(env = process.env) {
1646
+ return join(getHomeDir(env), ".hasna", APP);
1647
+ }
1648
+ function getResolverDataDir(env = process.env) {
1649
+ return dataDir({ app: APP, home: getHomeDir(env), env });
1650
+ }
1651
+ function adoptResolverDataDir(resolved, env = process.env) {
1652
+ const dataOverride = env.HASNA_DATA_HOME;
1653
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
1654
+ return true;
1655
+ return existsSync(join(resolved, STORE_DB_FILE));
1656
+ }
1657
+ function getEffectiveDataDir(env = process.env) {
1658
+ const resolved = getResolverDataDir(env);
1659
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
1660
+ }
1661
+
1637
1662
  // src/store.ts
1638
1663
  import { Database } from "bun:sqlite";
1639
1664
  import { mkdirSync } from "fs";
1640
- import { join } from "path";
1665
+ import { join as join2 } from "path";
1641
1666
  import { randomUUID } from "crypto";
1642
1667
  function nowIso2() {
1643
1668
  return new Date().toISOString();
1644
1669
  }
1645
- function openStore(dataDir) {
1646
- mkdirSync(dataDir, { recursive: true });
1647
- const db = new Database(join(dataDir, "workflows.db"));
1670
+ function openStore(dataDir2) {
1671
+ mkdirSync(dataDir2, { recursive: true });
1672
+ const db = new Database(join2(dataDir2, "workflows.db"));
1648
1673
  db.exec("PRAGMA journal_mode = WAL");
1649
1674
  db.exec("PRAGMA foreign_keys = ON");
1650
1675
  db.exec(`
@@ -1822,8 +1847,8 @@ function openStore(dataDir) {
1822
1847
 
1823
1848
  // src/wal.ts
1824
1849
  import { createHash as createHash3 } from "crypto";
1825
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1826
- import { join as join2 } from "path";
1850
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1851
+ import { join as join3 } from "path";
1827
1852
  var WAL_FILE_NAME = "session.wal";
1828
1853
  var SESSIONS_DIR_NAME = "sessions";
1829
1854
  var MAX_WAL_BYTES = 256 * 1024 * 1024;
@@ -1838,11 +1863,11 @@ class SessionWAL {
1838
1863
  constructor(filePath) {
1839
1864
  this.filePath = filePath;
1840
1865
  }
1841
- static open(dataDir) {
1842
- mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1843
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1844
- const legacy = join2(dataDir, WAL_FILE_NAME);
1845
- if (existsSync(legacy) && !existsSync(canonical)) {
1866
+ static open(dataDir2) {
1867
+ mkdirSync2(join3(dataDir2, SESSIONS_DIR_NAME), { recursive: true });
1868
+ const canonical = join3(dataDir2, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1869
+ const legacy = join3(dataDir2, WAL_FILE_NAME);
1870
+ if (existsSync2(legacy) && !existsSync2(canonical)) {
1846
1871
  try {
1847
1872
  renameSync(legacy, canonical);
1848
1873
  } catch {
@@ -1973,11 +1998,10 @@ function envInt(names, fallback) {
1973
1998
  return Number.isFinite(parsed) ? parsed : fallback;
1974
1999
  }
1975
2000
  function resolveWorkflowsConfig(overrides = {}) {
1976
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
1977
2001
  return {
1978
2002
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
1979
2003
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
1980
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2004
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
1981
2005
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
1982
2006
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
1983
2007
  };
package/dist/mcp/index.js CHANGED
@@ -1415,9 +1415,9 @@ class WorkflowsDaemon {
1415
1415
  }
1416
1416
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1417
1417
  if (!result.ok) {
1418
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1419
- if (retriesLeft > 0) {
1420
- this.store.bumpAttemptsNode(nodeRow.id);
1418
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1419
+ this.store.bumpAttemptsNode(nodeRow.id);
1420
+ if (shouldRetry) {
1421
1421
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1422
1422
  return { kind: "advanced", nodeId };
1423
1423
  }
@@ -1974,13 +1974,10 @@ async function callWorkflowTool(service, name, args) {
1974
1974
  throw new Error(`Unknown tool: ${name}`);
1975
1975
  }
1976
1976
  }
1977
-
1978
- // src/service.ts
1979
- import { join as join3 } from "path";
1980
1977
  // package.json
1981
1978
  var package_default = {
1982
1979
  name: "@hasna/workflows",
1983
- version: "0.1.2",
1980
+ version: "0.1.3",
1984
1981
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
1985
1982
  type: "module",
1986
1983
  main: "./dist/index.js",
@@ -2050,9 +2047,10 @@ var package_default = {
2050
2047
  author: "Hasna",
2051
2048
  license: "Apache-2.0",
2052
2049
  dependencies: {
2053
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
2050
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
2054
2051
  "@cursor/sdk": "^1.0.28",
2055
- "@openai/codex-sdk": "^0.147.0",
2052
+ "@hasna/paths": "0.1.0",
2053
+ "@openai/codex-sdk": "0.147.0",
2056
2054
  commander: "^13.1.0"
2057
2055
  },
2058
2056
  devDependencies: {
@@ -2064,6 +2062,33 @@ var package_default = {
2064
2062
  }
2065
2063
  };
2066
2064
 
2065
+ // src/paths.ts
2066
+ import { existsSync as existsSync2 } from "fs";
2067
+ import { homedir } from "os";
2068
+ import { join as join3 } from "path";
2069
+ import { dataDir } from "@hasna/paths";
2070
+ var APP = "workflows";
2071
+ var STORE_DB_FILE = "workflows.db";
2072
+ function getHomeDir(env = process.env) {
2073
+ return env["HOME"] || env["USERPROFILE"] || homedir();
2074
+ }
2075
+ function getLegacyDataDir(env = process.env) {
2076
+ return join3(getHomeDir(env), ".hasna", APP);
2077
+ }
2078
+ function getResolverDataDir(env = process.env) {
2079
+ return dataDir({ app: APP, home: getHomeDir(env), env });
2080
+ }
2081
+ function adoptResolverDataDir(resolved, env = process.env) {
2082
+ const dataOverride = env.HASNA_DATA_HOME;
2083
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
2084
+ return true;
2085
+ return existsSync2(join3(resolved, STORE_DB_FILE));
2086
+ }
2087
+ function getEffectiveDataDir(env = process.env) {
2088
+ const resolved = getResolverDataDir(env);
2089
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
2090
+ }
2091
+
2067
2092
  // src/service.ts
2068
2093
  var DEFAULT_PORT = 8790;
2069
2094
  var DEFAULT_HOST = "127.0.0.1";
@@ -2086,11 +2111,10 @@ function envInt(names, fallback) {
2086
2111
  return Number.isFinite(parsed) ? parsed : fallback;
2087
2112
  }
2088
2113
  function resolveWorkflowsConfig(overrides = {}) {
2089
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
2090
2114
  return {
2091
2115
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
2092
2116
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
2093
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2117
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
2094
2118
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
2095
2119
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
2096
2120
  };
@@ -440,13 +440,10 @@ var init_lanes = __esm(() => {
440
440
  ["grok", createGrokAdapter()]
441
441
  ]);
442
442
  });
443
-
444
- // src/service.ts
445
- import { join as join3 } from "path";
446
443
  // package.json
447
444
  var package_default = {
448
445
  name: "@hasna/workflows",
449
- version: "0.1.2",
446
+ version: "0.1.3",
450
447
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
451
448
  type: "module",
452
449
  main: "./dist/index.js",
@@ -516,9 +513,10 @@ var package_default = {
516
513
  author: "Hasna",
517
514
  license: "Apache-2.0",
518
515
  dependencies: {
519
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
516
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
520
517
  "@cursor/sdk": "^1.0.28",
521
- "@openai/codex-sdk": "^0.147.0",
518
+ "@hasna/paths": "0.1.0",
519
+ "@openai/codex-sdk": "0.147.0",
522
520
  commander: "^13.1.0"
523
521
  },
524
522
  devDependencies: {
@@ -1501,9 +1499,9 @@ class WorkflowsDaemon {
1501
1499
  }
1502
1500
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1503
1501
  if (!result.ok) {
1504
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1505
- if (retriesLeft > 0) {
1506
- this.store.bumpAttemptsNode(nodeRow.id);
1502
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1503
+ this.store.bumpAttemptsNode(nodeRow.id);
1504
+ if (shouldRetry) {
1507
1505
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1508
1506
  return { kind: "advanced", nodeId };
1509
1507
  }
@@ -1635,17 +1633,44 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1635
1633
  return store.getRun(run.id);
1636
1634
  }
1637
1635
 
1636
+ // src/paths.ts
1637
+ import { existsSync } from "fs";
1638
+ import { homedir } from "os";
1639
+ import { join } from "path";
1640
+ import { dataDir } from "@hasna/paths";
1641
+ var APP = "workflows";
1642
+ var STORE_DB_FILE = "workflows.db";
1643
+ function getHomeDir(env = process.env) {
1644
+ return env["HOME"] || env["USERPROFILE"] || homedir();
1645
+ }
1646
+ function getLegacyDataDir(env = process.env) {
1647
+ return join(getHomeDir(env), ".hasna", APP);
1648
+ }
1649
+ function getResolverDataDir(env = process.env) {
1650
+ return dataDir({ app: APP, home: getHomeDir(env), env });
1651
+ }
1652
+ function adoptResolverDataDir(resolved, env = process.env) {
1653
+ const dataOverride = env.HASNA_DATA_HOME;
1654
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
1655
+ return true;
1656
+ return existsSync(join(resolved, STORE_DB_FILE));
1657
+ }
1658
+ function getEffectiveDataDir(env = process.env) {
1659
+ const resolved = getResolverDataDir(env);
1660
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
1661
+ }
1662
+
1638
1663
  // src/store.ts
1639
1664
  import { Database } from "bun:sqlite";
1640
1665
  import { mkdirSync } from "fs";
1641
- import { join } from "path";
1666
+ import { join as join2 } from "path";
1642
1667
  import { randomUUID } from "crypto";
1643
1668
  function nowIso2() {
1644
1669
  return new Date().toISOString();
1645
1670
  }
1646
- function openStore(dataDir) {
1647
- mkdirSync(dataDir, { recursive: true });
1648
- const db = new Database(join(dataDir, "workflows.db"));
1671
+ function openStore(dataDir2) {
1672
+ mkdirSync(dataDir2, { recursive: true });
1673
+ const db = new Database(join2(dataDir2, "workflows.db"));
1649
1674
  db.exec("PRAGMA journal_mode = WAL");
1650
1675
  db.exec("PRAGMA foreign_keys = ON");
1651
1676
  db.exec(`
@@ -1823,8 +1848,8 @@ function openStore(dataDir) {
1823
1848
 
1824
1849
  // src/wal.ts
1825
1850
  import { createHash as createHash3 } from "crypto";
1826
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1827
- import { join as join2 } from "path";
1851
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1852
+ import { join as join3 } from "path";
1828
1853
  var WAL_FILE_NAME = "session.wal";
1829
1854
  var SESSIONS_DIR_NAME = "sessions";
1830
1855
  var MAX_WAL_BYTES = 256 * 1024 * 1024;
@@ -1839,11 +1864,11 @@ class SessionWAL {
1839
1864
  constructor(filePath) {
1840
1865
  this.filePath = filePath;
1841
1866
  }
1842
- static open(dataDir) {
1843
- mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1844
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1845
- const legacy = join2(dataDir, WAL_FILE_NAME);
1846
- if (existsSync(legacy) && !existsSync(canonical)) {
1867
+ static open(dataDir2) {
1868
+ mkdirSync2(join3(dataDir2, SESSIONS_DIR_NAME), { recursive: true });
1869
+ const canonical = join3(dataDir2, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1870
+ const legacy = join3(dataDir2, WAL_FILE_NAME);
1871
+ if (existsSync2(legacy) && !existsSync2(canonical)) {
1847
1872
  try {
1848
1873
  renameSync(legacy, canonical);
1849
1874
  } catch {
@@ -1974,11 +1999,10 @@ function envInt(names, fallback) {
1974
1999
  return Number.isFinite(parsed) ? parsed : fallback;
1975
2000
  }
1976
2001
  function resolveWorkflowsConfig(overrides = {}) {
1977
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
1978
2002
  return {
1979
2003
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
1980
2004
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
1981
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2005
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
1982
2006
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
1983
2007
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
1984
2008
  };
package/dist/service.js CHANGED
@@ -439,13 +439,10 @@ var init_lanes = __esm(() => {
439
439
  ["grok", createGrokAdapter()]
440
440
  ]);
441
441
  });
442
-
443
- // src/service.ts
444
- import { join as join3 } from "path";
445
442
  // package.json
446
443
  var package_default = {
447
444
  name: "@hasna/workflows",
448
- version: "0.1.2",
445
+ version: "0.1.3",
449
446
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
450
447
  type: "module",
451
448
  main: "./dist/index.js",
@@ -515,9 +512,10 @@ var package_default = {
515
512
  author: "Hasna",
516
513
  license: "Apache-2.0",
517
514
  dependencies: {
518
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
515
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
519
516
  "@cursor/sdk": "^1.0.28",
520
- "@openai/codex-sdk": "^0.147.0",
517
+ "@hasna/paths": "0.1.0",
518
+ "@openai/codex-sdk": "0.147.0",
521
519
  commander: "^13.1.0"
522
520
  },
523
521
  devDependencies: {
@@ -1500,9 +1498,9 @@ class WorkflowsDaemon {
1500
1498
  }
1501
1499
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1502
1500
  if (!result.ok) {
1503
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1504
- if (retriesLeft > 0) {
1505
- this.store.bumpAttemptsNode(nodeRow.id);
1501
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1502
+ this.store.bumpAttemptsNode(nodeRow.id);
1503
+ if (shouldRetry) {
1506
1504
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1507
1505
  return { kind: "advanced", nodeId };
1508
1506
  }
@@ -1634,17 +1632,44 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1634
1632
  return store.getRun(run.id);
1635
1633
  }
1636
1634
 
1635
+ // src/paths.ts
1636
+ import { existsSync } from "fs";
1637
+ import { homedir } from "os";
1638
+ import { join } from "path";
1639
+ import { dataDir } from "@hasna/paths";
1640
+ var APP = "workflows";
1641
+ var STORE_DB_FILE = "workflows.db";
1642
+ function getHomeDir(env = process.env) {
1643
+ return env["HOME"] || env["USERPROFILE"] || homedir();
1644
+ }
1645
+ function getLegacyDataDir(env = process.env) {
1646
+ return join(getHomeDir(env), ".hasna", APP);
1647
+ }
1648
+ function getResolverDataDir(env = process.env) {
1649
+ return dataDir({ app: APP, home: getHomeDir(env), env });
1650
+ }
1651
+ function adoptResolverDataDir(resolved, env = process.env) {
1652
+ const dataOverride = env.HASNA_DATA_HOME;
1653
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
1654
+ return true;
1655
+ return existsSync(join(resolved, STORE_DB_FILE));
1656
+ }
1657
+ function getEffectiveDataDir(env = process.env) {
1658
+ const resolved = getResolverDataDir(env);
1659
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
1660
+ }
1661
+
1637
1662
  // src/store.ts
1638
1663
  import { Database } from "bun:sqlite";
1639
1664
  import { mkdirSync } from "fs";
1640
- import { join } from "path";
1665
+ import { join as join2 } from "path";
1641
1666
  import { randomUUID } from "crypto";
1642
1667
  function nowIso2() {
1643
1668
  return new Date().toISOString();
1644
1669
  }
1645
- function openStore(dataDir) {
1646
- mkdirSync(dataDir, { recursive: true });
1647
- const db = new Database(join(dataDir, "workflows.db"));
1670
+ function openStore(dataDir2) {
1671
+ mkdirSync(dataDir2, { recursive: true });
1672
+ const db = new Database(join2(dataDir2, "workflows.db"));
1648
1673
  db.exec("PRAGMA journal_mode = WAL");
1649
1674
  db.exec("PRAGMA foreign_keys = ON");
1650
1675
  db.exec(`
@@ -1822,8 +1847,8 @@ function openStore(dataDir) {
1822
1847
 
1823
1848
  // src/wal.ts
1824
1849
  import { createHash as createHash3 } from "crypto";
1825
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1826
- import { join as join2 } from "path";
1850
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1851
+ import { join as join3 } from "path";
1827
1852
  var WAL_FILE_NAME = "session.wal";
1828
1853
  var SESSIONS_DIR_NAME = "sessions";
1829
1854
  var MAX_WAL_BYTES = 256 * 1024 * 1024;
@@ -1838,11 +1863,11 @@ class SessionWAL {
1838
1863
  constructor(filePath) {
1839
1864
  this.filePath = filePath;
1840
1865
  }
1841
- static open(dataDir) {
1842
- mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1843
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1844
- const legacy = join2(dataDir, WAL_FILE_NAME);
1845
- if (existsSync(legacy) && !existsSync(canonical)) {
1866
+ static open(dataDir2) {
1867
+ mkdirSync2(join3(dataDir2, SESSIONS_DIR_NAME), { recursive: true });
1868
+ const canonical = join3(dataDir2, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1869
+ const legacy = join3(dataDir2, WAL_FILE_NAME);
1870
+ if (existsSync2(legacy) && !existsSync2(canonical)) {
1846
1871
  try {
1847
1872
  renameSync(legacy, canonical);
1848
1873
  } catch {
@@ -1973,11 +1998,10 @@ function envInt(names, fallback) {
1973
1998
  return Number.isFinite(parsed) ? parsed : fallback;
1974
1999
  }
1975
2000
  function resolveWorkflowsConfig(overrides = {}) {
1976
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
1977
2001
  return {
1978
2002
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
1979
2003
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
1980
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2004
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
1981
2005
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
1982
2006
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
1983
2007
  };
@@ -4,7 +4,7 @@
4
4
  "class": "service",
5
5
  "contractVersion": "v1",
6
6
  "kitVersion": "0.14.1",
7
- "description": "Universal graph workflow app: graph language (start/step/decision/while/end), three-table run store, session WAL, claims/leases daemon, four lane adapters (claude, codex, cursor, grok), and a full CLI on four surfaces (CLI, MCP, serve, SDK).",
7
+ "description": "Universal graph workflow app: graph language (start/step/decision/while/end), three-table run store, session WAL, claims/leases daemon, four lane adapters (claude, codex, cursor, grok), and a full CLI on four surfaces (CLI, MCP, serve, SDK). Local state lives at the effective workflows data home (~/.hasna/workflows by default, resolved via @hasna/paths; the XDG data home is adopted once the store is migrated there or HASNA_DATA_HOME is set).",
8
8
  "bins": [
9
9
  "workflows",
10
10
  "workflows-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/workflows",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Universal graph workflow app — graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -70,9 +70,10 @@
70
70
  "author": "Hasna",
71
71
  "license": "Apache-2.0",
72
72
  "dependencies": {
73
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
73
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
74
74
  "@cursor/sdk": "^1.0.28",
75
- "@openai/codex-sdk": "^0.147.0",
75
+ "@hasna/paths": "0.1.0",
76
+ "@openai/codex-sdk": "0.147.0",
76
77
  "commander": "^13.1.0"
77
78
  },
78
79
  "devDependencies": {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Workflows data-home resolution — the effective local data home for the
3
+ * @hasna/workflows package, resolved through @hasna/paths.
4
+ *
5
+ * Resolution order:
6
+ * 1. exact-app override — `HASNA_WORKFLOWS_DATA_DIR` / `WORKFLOWS_DATA_DIR`
7
+ * (the shipped overrides, applied in `resolveWorkflowsConfig`) — wins
8
+ * unconditionally;
9
+ * 2. the @hasna/paths data home (XDG / macOS home layout) once adopted;
10
+ * 3. otherwise the legacy `~/.hasna/workflows` default.
11
+ *
12
+ * Gated legacy adoption: the legacy `~/.hasna/workflows` home stays effective
13
+ * until the store has actually been migrated to the XDG data home
14
+ * (`workflows.db` exists at the resolver home) or the operator sets the
15
+ * data-kind override `HASNA_DATA_HOME`. An existing local store never becomes
16
+ * invisible on upgrade.
17
+ */
18
+ /** The workflows app slug used by the @hasna/paths resolver. */
19
+ export declare const APP: "workflows";
20
+ /** The store database file name, the marker that a store lives at a data home. */
21
+ export declare const STORE_DB_FILE = "workflows.db";
22
+ /**
23
+ * The effective user home, mirroring the pre-existing resolution
24
+ * (`HOME` || `USERPROFILE` || os.homedir()).
25
+ */
26
+ export declare function getHomeDir(env?: NodeJS.ProcessEnv): string;
27
+ /** Pre-XDG legacy workflows data home: ~/.hasna/workflows. */
28
+ export declare function getLegacyDataDir(env?: NodeJS.ProcessEnv): string;
29
+ /**
30
+ * The @hasna/paths-resolved workflows data home (XDG / macOS home layout):
31
+ * `~/.local/share/hasna/workflows` on Linux, `~/Library/Application
32
+ * Support/Hasna/workflows` on macOS. The home override mirrors the
33
+ * pre-existing `$HOME`-first resolution so the resolver follows the same home
34
+ * the legacy path does.
35
+ */
36
+ export declare function getResolverDataDir(env?: NodeJS.ProcessEnv): string;
37
+ /**
38
+ * Whether the resolver (XDG) data home should be adopted as the effective
39
+ * workflows data home. The resolver home is adopted only when the operator
40
+ * has set `HASNA_DATA_HOME` (the data-kind override — a deliberate opt-in to
41
+ * the XDG layout) or the store has already been physically migrated there
42
+ * (`workflows.db` exists). A machine that only redirects another kind (e.g.
43
+ * cache to tmpfs) must NOT have its data home moved, and a live store at the
44
+ * legacy home must never become invisible on upgrade.
45
+ */
46
+ export declare function adoptResolverDataDir(resolved: string, env?: NodeJS.ProcessEnv): boolean;
47
+ /**
48
+ * The effective workflows data home: the resolver home once adopted,
49
+ * otherwise the legacy `~/.hasna/workflows` default.
50
+ */
51
+ export declare function getEffectiveDataDir(env?: NodeJS.ProcessEnv): string;
package/types/types.d.ts CHANGED
@@ -12,7 +12,9 @@ export interface WorkflowsConfig {
12
12
  port: number;
13
13
  /** Bind host for workflows-serve. Default 127.0.0.1. */
14
14
  host: string;
15
- /** Data directory (store slice lands the db here). Default ~/.hasna/workflows. */
15
+ /** Data directory (store slice lands the db here). Default ~/.hasna/workflows,
16
+ * resolved through @hasna/paths once the XDG data home is adopted
17
+ * (HASNA_DATA_HOME set or the store migrated there). */
16
18
  dataDir: string;
17
19
  /** Optional client API URL (WORKFLOWS_API_URL) — consumed by later slices. */
18
20
  apiUrl?: string;