jfl 0.5.0 → 0.6.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.
Files changed (135) hide show
  1. package/dist/commands/context-hub.d.ts +1 -0
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +246 -2
  4. package/dist/commands/context-hub.js.map +1 -1
  5. package/dist/commands/peter.d.ts +2 -0
  6. package/dist/commands/peter.d.ts.map +1 -1
  7. package/dist/commands/peter.js +242 -52
  8. package/dist/commands/peter.js.map +1 -1
  9. package/dist/commands/setup.d.ts +12 -0
  10. package/dist/commands/setup.d.ts.map +1 -0
  11. package/dist/commands/setup.js +322 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/commands/train.d.ts +33 -0
  14. package/dist/commands/train.d.ts.map +1 -0
  15. package/dist/commands/train.js +510 -0
  16. package/dist/commands/train.js.map +1 -0
  17. package/dist/commands/verify.d.ts +14 -0
  18. package/dist/commands/verify.d.ts.map +1 -0
  19. package/dist/commands/verify.js +276 -0
  20. package/dist/commands/verify.js.map +1 -0
  21. package/dist/dashboard-static/assets/index-CW9ZxqX8.css +1 -0
  22. package/dist/dashboard-static/assets/index-DNN__p4K.js +121 -0
  23. package/dist/dashboard-static/index.html +2 -2
  24. package/dist/index.js +99 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/agent-session.d.ts.map +1 -1
  27. package/dist/lib/agent-session.js +12 -4
  28. package/dist/lib/agent-session.js.map +1 -1
  29. package/dist/lib/eval-snapshot.js +1 -1
  30. package/dist/lib/eval-snapshot.js.map +1 -1
  31. package/dist/lib/pi-sky/bridge.d.ts +55 -0
  32. package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
  33. package/dist/lib/pi-sky/bridge.js +264 -0
  34. package/dist/lib/pi-sky/bridge.js.map +1 -0
  35. package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
  36. package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
  37. package/dist/lib/pi-sky/cost-monitor.js +126 -0
  38. package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
  39. package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
  40. package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
  41. package/dist/lib/pi-sky/eval-sweep.js +141 -0
  42. package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
  43. package/dist/lib/pi-sky/event-router.d.ts +32 -0
  44. package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
  45. package/dist/lib/pi-sky/event-router.js +176 -0
  46. package/dist/lib/pi-sky/event-router.js.map +1 -0
  47. package/dist/lib/pi-sky/experiment.d.ts +9 -0
  48. package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
  49. package/dist/lib/pi-sky/experiment.js +83 -0
  50. package/dist/lib/pi-sky/experiment.js.map +1 -0
  51. package/dist/lib/pi-sky/index.d.ts +16 -0
  52. package/dist/lib/pi-sky/index.d.ts.map +1 -0
  53. package/dist/lib/pi-sky/index.js +16 -0
  54. package/dist/lib/pi-sky/index.js.map +1 -0
  55. package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
  56. package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
  57. package/dist/lib/pi-sky/stratus-gate.js +61 -0
  58. package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
  59. package/dist/lib/pi-sky/swarm.d.ts +28 -0
  60. package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
  61. package/dist/lib/pi-sky/swarm.js +208 -0
  62. package/dist/lib/pi-sky/swarm.js.map +1 -0
  63. package/dist/lib/pi-sky/types.d.ts +139 -0
  64. package/dist/lib/pi-sky/types.d.ts.map +1 -0
  65. package/dist/lib/pi-sky/types.js +2 -0
  66. package/dist/lib/pi-sky/types.js.map +1 -0
  67. package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
  68. package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
  69. package/dist/lib/pi-sky/voice-bridge.js +91 -0
  70. package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
  71. package/dist/lib/policy-head.d.ts +16 -1
  72. package/dist/lib/policy-head.d.ts.map +1 -1
  73. package/dist/lib/policy-head.js +117 -19
  74. package/dist/lib/policy-head.js.map +1 -1
  75. package/dist/lib/predictor.d.ts +10 -0
  76. package/dist/lib/predictor.d.ts.map +1 -1
  77. package/dist/lib/predictor.js +46 -7
  78. package/dist/lib/predictor.js.map +1 -1
  79. package/dist/lib/setup/agent-generator.d.ts +18 -0
  80. package/dist/lib/setup/agent-generator.d.ts.map +1 -0
  81. package/dist/lib/setup/agent-generator.js +114 -0
  82. package/dist/lib/setup/agent-generator.js.map +1 -0
  83. package/dist/lib/setup/context-analyzer.d.ts +16 -0
  84. package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
  85. package/dist/lib/setup/context-analyzer.js +112 -0
  86. package/dist/lib/setup/context-analyzer.js.map +1 -0
  87. package/dist/lib/setup/doc-auditor.d.ts +54 -0
  88. package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
  89. package/dist/lib/setup/doc-auditor.js +629 -0
  90. package/dist/lib/setup/doc-auditor.js.map +1 -0
  91. package/dist/lib/setup/domain-generator.d.ts +7 -0
  92. package/dist/lib/setup/domain-generator.d.ts.map +1 -0
  93. package/dist/lib/setup/domain-generator.js +58 -0
  94. package/dist/lib/setup/domain-generator.js.map +1 -0
  95. package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
  96. package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
  97. package/dist/lib/setup/smart-eval-generator.js +378 -0
  98. package/dist/lib/setup/smart-eval-generator.js.map +1 -0
  99. package/dist/lib/setup/smart-recommender.d.ts +63 -0
  100. package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
  101. package/dist/lib/setup/smart-recommender.js +329 -0
  102. package/dist/lib/setup/smart-recommender.js.map +1 -0
  103. package/dist/lib/setup/spec-generator.d.ts +63 -0
  104. package/dist/lib/setup/spec-generator.d.ts.map +1 -0
  105. package/dist/lib/setup/spec-generator.js +310 -0
  106. package/dist/lib/setup/spec-generator.js.map +1 -0
  107. package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
  108. package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
  109. package/dist/lib/setup/violation-agent-generator.js +255 -0
  110. package/dist/lib/setup/violation-agent-generator.js.map +1 -0
  111. package/package.json +1 -1
  112. package/packages/pi/extensions/context.ts +88 -55
  113. package/packages/pi/extensions/hub-resolver.ts +63 -0
  114. package/packages/pi/extensions/index.ts +16 -3
  115. package/packages/pi/extensions/memory-tool.ts +9 -4
  116. package/packages/pi/extensions/session.ts +68 -16
  117. package/packages/pi/extensions/tool-renderers.ts +6 -5
  118. package/scripts/train/requirements.txt +5 -0
  119. package/scripts/train/train-policy-head.py +477 -0
  120. package/scripts/train/v2/dataset.py +81 -0
  121. package/scripts/train/v2/domain.json +18 -0
  122. package/scripts/train/v2/eval.py +196 -0
  123. package/scripts/train/v2/generate_data.py +219 -0
  124. package/scripts/train/v2/infer.py +188 -0
  125. package/scripts/train/v2/model.py +112 -0
  126. package/scripts/train/v2/precompute.py +132 -0
  127. package/scripts/train/v2/train.py +302 -0
  128. package/scripts/train/v2/transform_buffer.py +227 -0
  129. package/scripts/train/v2/validate_data.py +115 -0
  130. package/template/.claude/settings.json +2 -15
  131. package/template/scripts/session/session-cleanup.sh +2 -11
  132. package/template/scripts/session/session-end-hub.sh +72 -0
  133. package/template/scripts/session/session-start-hub.sh +105 -0
  134. package/dist/dashboard-static/assets/index-B6b867Pv.js +0 -121
  135. package/dist/dashboard-static/assets/index-Y4BrqxV-.css +0 -1
@@ -17,5 +17,6 @@ export declare function contextHubCommand(action?: string, options?: {
17
17
  port?: number;
18
18
  global?: boolean;
19
19
  quiet?: boolean;
20
+ projectRoot?: string;
20
21
  }): Promise<void>;
21
22
  //# sourceMappingURL=context-hub.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-hub.d.ts","sourceRoot":"","sources":["../../src/commands/context-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAguEH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBjF;AAkMD,wBAAsB,qBAAqB,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAiHxF;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,iBA8lBnE"}
1
+ {"version":3,"file":"context-hub.d.ts","sourceRoot":"","sources":["../../src/commands/context-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAy9EH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBjF;AAkMD,wBAAsB,qBAAqB,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAiHxF;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,iBA8lBzF"}
@@ -547,6 +547,20 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
547
547
  res.end(JSON.stringify({ status: "ok", port }));
548
548
  return;
549
549
  }
550
+ // Setup report — serves .jfl/setup-report.json for dashboard consumption
551
+ if (url.pathname === "/api/setup-report" && req.method === "GET") {
552
+ const reportPath = path.join(projectRoot, ".jfl", "setup-report.json");
553
+ if (fs.existsSync(reportPath)) {
554
+ const report = fs.readFileSync(reportPath, "utf-8");
555
+ res.writeHead(200, { "Content-Type": "application/json" });
556
+ res.end(report);
557
+ }
558
+ else {
559
+ res.writeHead(404, { "Content-Type": "application/json" });
560
+ res.end(JSON.stringify({ error: "No setup report. Run: jfl setup" }));
561
+ }
562
+ return;
563
+ }
550
564
  // Dashboard - served without API auth (has its own token flow in JS)
551
565
  if (url.pathname.startsWith("/dashboard")) {
552
566
  import("../dashboard/index.js").then(({ handleDashboardRoutes }) => {
@@ -1785,6 +1799,236 @@ function createServer(projectRoot, port, eventBus, flowEngine) {
1785
1799
  });
1786
1800
  return;
1787
1801
  }
1802
+ // ── Session Parity API ────────────────────────────────────────────
1803
+ // These endpoints provide runtime-agnostic session lifecycle.
1804
+ // Both Claude Code hooks and Pi extensions call these instead of
1805
+ // duplicating logic. Single source of truth for session init/end.
1806
+ // POST /api/session/init — sync repos, create branch, run doctor
1807
+ if (url.pathname === "/api/session/init" && req.method === "POST") {
1808
+ let body = "";
1809
+ req.on("data", (chunk) => body += chunk);
1810
+ req.on("end", async () => {
1811
+ try {
1812
+ const { runtime } = JSON.parse(body || "{}");
1813
+ const warnings = [];
1814
+ const scriptDir = path.join(projectRoot, "scripts", "session");
1815
+ // Step 1: Sync repos
1816
+ const syncScript = path.join(scriptDir, "session-sync.sh");
1817
+ let syncOk = true;
1818
+ if (fs.existsSync(syncScript)) {
1819
+ try {
1820
+ execSync(`bash "${syncScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
1821
+ }
1822
+ catch (err) {
1823
+ syncOk = false;
1824
+ warnings.push(`Sync warning: ${(err.message || "").split("\n")[0]}`);
1825
+ }
1826
+ }
1827
+ // Step 2: Doctor check
1828
+ const doctorScript = path.join(scriptDir, "jfl-doctor.sh");
1829
+ let doctorErrors = 0;
1830
+ let doctorWarnings = 0;
1831
+ if (fs.existsSync(doctorScript)) {
1832
+ try {
1833
+ const doctorOut = execSync(`bash "${doctorScript}"`, { cwd: projectRoot, timeout: 30000, stdio: ["pipe", "pipe", "pipe"] }).toString();
1834
+ const em = doctorOut.match(/(\d+) error\(s\)/);
1835
+ const wm = doctorOut.match(/(\d+) warning\(s\)/);
1836
+ doctorErrors = em ? parseInt(em[1]) : 0;
1837
+ doctorWarnings = wm ? parseInt(wm[1]) : 0;
1838
+ if (doctorErrors > 0)
1839
+ warnings.push(`Doctor: ${doctorErrors} errors`);
1840
+ }
1841
+ catch { }
1842
+ }
1843
+ // Step 3: Create session branch
1844
+ let currentBranch = "";
1845
+ try {
1846
+ currentBranch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1847
+ }
1848
+ catch { }
1849
+ let sessionBranch = currentBranch;
1850
+ if (!currentBranch.startsWith("session-")) {
1851
+ const user = (() => {
1852
+ try {
1853
+ return execSync("git config user.name", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] })
1854
+ .toString().trim().replace(/\s+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 30) || "user";
1855
+ }
1856
+ catch {
1857
+ return "user";
1858
+ }
1859
+ })();
1860
+ const now = new Date();
1861
+ const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
1862
+ const timeStr = now.toISOString().slice(11, 16).replace(":", "");
1863
+ const randomId = Math.random().toString(16).slice(2, 8);
1864
+ sessionBranch = `session-${user}-${dateStr}-${timeStr}-${randomId}`;
1865
+ try {
1866
+ execSync(`git checkout -b "${sessionBranch}"`, { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] });
1867
+ }
1868
+ catch {
1869
+ sessionBranch = currentBranch || "main";
1870
+ warnings.push("Could not create session branch");
1871
+ }
1872
+ }
1873
+ // Step 4: Save session info
1874
+ const jflDir = path.join(projectRoot, ".jfl");
1875
+ fs.mkdirSync(path.join(jflDir, "logs"), { recursive: true });
1876
+ fs.mkdirSync(path.join(jflDir, "journal"), { recursive: true });
1877
+ fs.writeFileSync(path.join(jflDir, "current-session-branch.txt"), sessionBranch);
1878
+ fs.writeFileSync(path.join(jflDir, "current-worktree.txt"), "direct");
1879
+ // Emit session start event
1880
+ if (eventBus) {
1881
+ eventBus.emit({
1882
+ type: "session:started",
1883
+ source: `hub:${runtime || "unknown"}`,
1884
+ data: { branch: sessionBranch, runtime, warnings },
1885
+ });
1886
+ }
1887
+ res.writeHead(200, { "Content-Type": "application/json" });
1888
+ res.end(JSON.stringify({
1889
+ ok: true,
1890
+ branch: sessionBranch,
1891
+ syncOk,
1892
+ doctor: { errors: doctorErrors, warnings: doctorWarnings },
1893
+ warnings,
1894
+ }));
1895
+ }
1896
+ catch (err) {
1897
+ res.writeHead(500, { "Content-Type": "application/json" });
1898
+ res.end(JSON.stringify({ error: err.message }));
1899
+ }
1900
+ });
1901
+ return;
1902
+ }
1903
+ // POST /api/prompt — get system prompt injection (CLAUDE.md + context)
1904
+ if (url.pathname === "/api/prompt" && req.method === "POST") {
1905
+ let body = "";
1906
+ req.on("data", (chunk) => body += chunk);
1907
+ req.on("end", async () => {
1908
+ try {
1909
+ const { taskType, maxItems } = JSON.parse(body || "{}");
1910
+ const parts = [];
1911
+ // 1. Load CLAUDE.md
1912
+ const claudeMdPath = path.join(projectRoot, "CLAUDE.md");
1913
+ if (fs.existsSync(claudeMdPath)) {
1914
+ const content = fs.readFileSync(claudeMdPath, "utf-8");
1915
+ if (content.length > 50000) {
1916
+ const sections = content.split(/^## /m);
1917
+ const critical = sections.filter((s) => /CRITICAL|Session Sync|Journal Protocol|Immediate Decision|Working Mode|Core Architecture/i.test(s.slice(0, 100)));
1918
+ if (critical.length > 0) {
1919
+ parts.push("# Project Instructions (CLAUDE.md — critical sections)\n");
1920
+ parts.push("## " + critical.join("\n\n## "));
1921
+ }
1922
+ else {
1923
+ parts.push("# Project Instructions (CLAUDE.md — truncated)\n");
1924
+ parts.push(content.slice(0, 30000));
1925
+ }
1926
+ }
1927
+ else {
1928
+ parts.push("# Project Instructions (CLAUDE.md)\n");
1929
+ parts.push(content);
1930
+ }
1931
+ }
1932
+ // 2. Load recent context
1933
+ try {
1934
+ const contextItems = readJournalEntries(projectRoot, maxItems ?? 20);
1935
+ if (contextItems.length > 0) {
1936
+ parts.push("\n## Recent Project Context\n");
1937
+ parts.push(contextItems.map(item => {
1938
+ const prefix = item.source ? `[${item.source}] ` : "";
1939
+ return `${prefix}${item.content}`;
1940
+ }).join("\n\n"));
1941
+ }
1942
+ }
1943
+ catch { }
1944
+ // 3. Load knowledge docs summaries
1945
+ const knowledgeDir = path.join(projectRoot, "knowledge");
1946
+ if (fs.existsSync(knowledgeDir)) {
1947
+ const knowledgeDocs = ["VISION.md", "ROADMAP.md", "NARRATIVE.md", "THESIS.md"];
1948
+ const summaries = [];
1949
+ for (const doc of knowledgeDocs) {
1950
+ const docPath = path.join(knowledgeDir, doc);
1951
+ if (fs.existsSync(docPath)) {
1952
+ const content = fs.readFileSync(docPath, "utf-8");
1953
+ if (content.length > 100) {
1954
+ summaries.push(`### ${doc}\n${content.slice(0, 500)}${content.length > 500 ? "\n..." : ""}`);
1955
+ }
1956
+ }
1957
+ }
1958
+ if (summaries.length > 0) {
1959
+ parts.push("\n## Knowledge Documents\n");
1960
+ parts.push(summaries.join("\n\n"));
1961
+ }
1962
+ }
1963
+ res.writeHead(200, { "Content-Type": "application/json" });
1964
+ res.end(JSON.stringify({
1965
+ prompt: parts.join("\n"),
1966
+ claudeMdSize: fs.existsSync(path.join(projectRoot, "CLAUDE.md"))
1967
+ ? fs.statSync(path.join(projectRoot, "CLAUDE.md")).size : 0,
1968
+ taskType: taskType ?? "general",
1969
+ }));
1970
+ }
1971
+ catch (err) {
1972
+ res.writeHead(500, { "Content-Type": "application/json" });
1973
+ res.end(JSON.stringify({ error: err.message }));
1974
+ }
1975
+ });
1976
+ return;
1977
+ }
1978
+ // POST /api/session/end — cleanup session (merge, commit, etc)
1979
+ if (url.pathname === "/api/session/end" && req.method === "POST") {
1980
+ let body = "";
1981
+ req.on("data", (chunk) => body += chunk);
1982
+ req.on("end", async () => {
1983
+ try {
1984
+ const { runtime, skipCleanup } = JSON.parse(body || "{}");
1985
+ // Check journal exists
1986
+ let hasJournal = false;
1987
+ try {
1988
+ const branch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
1989
+ const journalPath = path.join(projectRoot, ".jfl", "journal", `${branch}.jsonl`);
1990
+ hasJournal = fs.existsSync(journalPath) && fs.statSync(journalPath).size > 0;
1991
+ }
1992
+ catch { }
1993
+ let cleanupResult = "skipped";
1994
+ if (!skipCleanup) {
1995
+ const cleanupScript = path.join(projectRoot, "scripts", "session", "session-cleanup.sh");
1996
+ if (fs.existsSync(cleanupScript)) {
1997
+ try {
1998
+ execSync(`bash "${cleanupScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
1999
+ cleanupResult = "ok";
2000
+ }
2001
+ catch (err) {
2002
+ cleanupResult = `error: ${(err.message || "").split("\n")[0]}`;
2003
+ }
2004
+ }
2005
+ else {
2006
+ cleanupResult = "no cleanup script";
2007
+ }
2008
+ }
2009
+ // Emit session end event
2010
+ if (eventBus) {
2011
+ eventBus.emit({
2012
+ type: "session:ended",
2013
+ source: `hub:${runtime || "unknown"}`,
2014
+ data: { hasJournal, cleanupResult, runtime },
2015
+ });
2016
+ }
2017
+ res.writeHead(200, { "Content-Type": "application/json" });
2018
+ res.end(JSON.stringify({
2019
+ ok: true,
2020
+ hasJournal,
2021
+ cleanupResult,
2022
+ warnings: hasJournal ? [] : ["No journal entry for this session"],
2023
+ }));
2024
+ }
2025
+ catch (err) {
2026
+ res.writeHead(500, { "Content-Type": "application/json" });
2027
+ res.end(JSON.stringify({ error: err.message }));
2028
+ }
2029
+ });
2030
+ return;
2031
+ }
1788
2032
  // ── Findings API ──────────────────────────────────────────────────
1789
2033
  // GET /api/v1/findings — list current findings
1790
2034
  if (url.pathname === "/api/v1/findings" && req.method === "GET") {
@@ -2060,7 +2304,7 @@ async function startDaemon(projectRoot, port) {
2060
2304
  }
2061
2305
  // Start as detached process with CONTEXT_HUB_DAEMON=1 so the serve
2062
2306
  // action knows to ignore SIGTERM during its startup grace period
2063
- const child = spawn(jflCmd, ["context-hub", "serve", "--port", String(port)], {
2307
+ const child = spawn(jflCmd, ["context-hub", "serve", "--port", String(port), "--project-root", projectRoot], {
2064
2308
  cwd: projectRoot,
2065
2309
  detached: true,
2066
2310
  stdio: ["ignore", fs.openSync(logFile, "a"), fs.openSync(logFile, "a")],
@@ -2245,7 +2489,7 @@ export async function ensureDaemonInstalled(opts) {
2245
2489
  // ============================================================================
2246
2490
  export async function contextHubCommand(action, options = {}) {
2247
2491
  const isGlobal = options.global || false;
2248
- const projectRoot = isGlobal ? homedir() : process.cwd();
2492
+ const projectRoot = options.projectRoot || (isGlobal ? homedir() : process.cwd());
2249
2493
  const port = options.port || getProjectPort(projectRoot);
2250
2494
  // Ensure directories exist (skip for actions that don't need local project root)
2251
2495
  const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];