@yorz/cli 0.3.5 → 0.3.6

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 (67) hide show
  1. package/README.md +17 -17
  2. package/dist/cli/index.js +657 -201
  3. package/dist/gui/assets/{abnfDiagram-VRR7QNED-DQM-IUdR.js → abnfDiagram-VRR7QNED-xc0C50rB.js} +1 -1
  4. package/dist/gui/assets/{arc-WoQS9inO.js → arc-BBPWDS5i.js} +1 -1
  5. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-BSJfE6nY.js → architectureDiagram-ZJ3FMSHR-UfjPkiOc.js} +1 -1
  6. package/dist/gui/assets/{blockDiagram-677ZJIJ3-BXk0h_Na.js → blockDiagram-677ZJIJ3-D4-uGWRR.js} +1 -1
  7. package/dist/gui/assets/{c4Diagram-LMCZKHZV-Sgnopqnh.js → c4Diagram-LMCZKHZV-DOsjz28F.js} +1 -1
  8. package/dist/gui/assets/channel-BUjCRtd2.js +1 -0
  9. package/dist/gui/assets/{chunk-2Q5K7J3B-sWwRHI31.js → chunk-2Q5K7J3B-D8U1wDI8.js} +1 -1
  10. package/dist/gui/assets/{chunk-32BRIVSS-6HJXOWs9.js → chunk-32BRIVSS-B_fOO0A_.js} +1 -1
  11. package/dist/gui/assets/{chunk-5VM5RSS4-De_Y0Itz.js → chunk-5VM5RSS4-BEs0La2r.js} +1 -1
  12. package/dist/gui/assets/{chunk-EX3LRPZG-CTtRnKWx.js → chunk-EX3LRPZG-tp9GO_-e.js} +1 -1
  13. package/dist/gui/assets/{chunk-JWPE2WC7-Dp9usWZX.js → chunk-JWPE2WC7-CtyvW3SG.js} +1 -1
  14. package/dist/gui/assets/{chunk-MOJQB5TN-OoM3V1FE.js → chunk-MOJQB5TN-511Q-LZH.js} +1 -1
  15. package/dist/gui/assets/{chunk-RYQCIY6F-M8me5HDP.js → chunk-RYQCIY6F-tBbgfa0S.js} +1 -1
  16. package/dist/gui/assets/{chunk-V7JOEXUC-CLdvRo84.js → chunk-V7JOEXUC-BrxVSk_j.js} +1 -1
  17. package/dist/gui/assets/{chunk-VR4S4FIN-BMEciG-B.js → chunk-VR4S4FIN-D933YBYm.js} +1 -1
  18. package/dist/gui/assets/{chunk-XXDRQBXY-DT8b8q8Q.js → chunk-XXDRQBXY-BFQVCR7q.js} +1 -1
  19. package/dist/gui/assets/classDiagram-OUVF2IWQ-CbHDPN3x.js +1 -0
  20. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CbHDPN3x.js +1 -0
  21. package/dist/gui/assets/{cose-bilkent-JH36ORCC-C1w9npQD.js → cose-bilkent-JH36ORCC-CvG96QUS.js} +1 -1
  22. package/dist/gui/assets/{cynefin-VYW2F7L2-buyB6Ypi.js → cynefin-VYW2F7L2-D1hO5-bj.js} +1 -1
  23. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DPQmwOX9.js → cynefinDiagram-TSTJHNR4-CGlQfxl6.js} +1 -1
  24. package/dist/gui/assets/{dagre-VKFMJZFB-DfbmQsg3.js → dagre-VKFMJZFB-DONzuwX_.js} +1 -1
  25. package/dist/gui/assets/{diagram-FQU43EPY-DZZkoYxF.js → diagram-FQU43EPY-HAAtgUO1.js} +1 -1
  26. package/dist/gui/assets/{diagram-G47NLZAW-DJyFDLgi.js → diagram-G47NLZAW-BXhu9UFM.js} +1 -1
  27. package/dist/gui/assets/{diagram-NH7WQ7WH-CeIoika9.js → diagram-NH7WQ7WH-C4ERZVd_.js} +1 -1
  28. package/dist/gui/assets/{diagram-OA4YK3LP-D3bJmzbA.js → diagram-OA4YK3LP-w9tIMMGB.js} +1 -1
  29. package/dist/gui/assets/{diagram-WEI45ONY-5whYnN6Z.js → diagram-WEI45ONY-CaAeYUbA.js} +1 -1
  30. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-fRcOahPq.js → ebnfDiagram-CCIWWBDH-B6W0V0qu.js} +1 -1
  31. package/dist/gui/assets/{erDiagram-Q63AITRT-h22ShxWi.js → erDiagram-Q63AITRT-C3sskZRU.js} +1 -1
  32. package/dist/gui/assets/{flowDiagram-23GEKE2U-CImBEPKl.js → flowDiagram-23GEKE2U-C104zn-S.js} +1 -1
  33. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-D7GfEoL1.js → ganttDiagram-NO4QXBWP-BTCssrDx.js} +1 -1
  34. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-BQlPqwV3.js → gitGraphDiagram-IHSO6WYX-DjOOeH7D.js} +1 -1
  35. package/dist/gui/assets/{index-BnMXI8QL.js → index-DxqBnHJb.js} +27 -27
  36. package/dist/gui/assets/index-S1uf4yLT.css +1 -0
  37. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-dQgAvdfE.js → infoDiagram-FWYZ7A6U-BmFWZBS4.js} +1 -1
  38. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-Dsb8okiC.js → ishikawaDiagram-FXEZZL3T-CDxcUzdH.js} +1 -1
  39. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BVaJ8kFx.js → journeyDiagram-5HDEW3XC-D-6O7chT.js} +1 -1
  40. package/dist/gui/assets/{kanban-definition-HUTT4EX6-CtRRLwap.js → kanban-definition-HUTT4EX6-B8x7k6Fv.js} +1 -1
  41. package/dist/gui/assets/{linear-CtTzoPCE.js → linear-D5Ymya6M.js} +1 -1
  42. package/dist/gui/assets/{mermaid.core-BO-VJXe-.js → mermaid.core-DufPaTEv.js} +4 -4
  43. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-Djl0w9Df.js → mindmap-definition-LN4V7U3C-DtvTXeH1.js} +1 -1
  44. package/dist/gui/assets/{pegDiagram-2B236MQR-D2VRUT1H.js → pegDiagram-2B236MQR-CC4Ckg2m.js} +1 -1
  45. package/dist/gui/assets/{pieDiagram-ENE6RG2P-Bp7Ytwe1.js → pieDiagram-ENE6RG2P-CbjSsDIy.js} +1 -1
  46. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Df4S5Uw8.js → quadrantDiagram-ABIIQ3AL-BxPwfoi7.js} +1 -1
  47. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-1E9T3-eU.js → railroadDiagram-RFXS5EU6-YX-VS0Vh.js} +1 -1
  48. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-ClWnTEWR.js → requirementDiagram-TGXJPOKE-BEtW3t1o.js} +1 -1
  49. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-kV-szT-S.js → sankeyDiagram-HTMAVEWB-ChxRWgoD.js} +1 -1
  50. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C17izAQ5.js → sequenceDiagram-DBY2YBRQ-_fnT_6l0.js} +1 -1
  51. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-Qa6aDRpG.js → sizeCapture-X5ZJPWSS-GrOKZWai.js} +1 -1
  52. package/dist/gui/assets/{stateDiagram-2N3HPSRC-C84qUVRJ.js → stateDiagram-2N3HPSRC-CXQbtXQA.js} +1 -1
  53. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-BvJQB9Ga.js +1 -0
  54. package/dist/gui/assets/{swimlanes-5IMT3BWC-BByVALRE.js → swimlanes-5IMT3BWC-CNBWQS8A.js} +2 -2
  55. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-CnSljsJi.js +8 -0
  56. package/dist/gui/assets/{timeline-definition-FHXFAJF6-BD9jOAZN.js → timeline-definition-FHXFAJF6-Kvxm3vNr.js} +1 -1
  57. package/dist/gui/assets/{vennDiagram-L72KCM5P-BqTRSqDu.js → vennDiagram-L72KCM5P-BzoVFyz9.js} +1 -1
  58. package/dist/gui/assets/{wardleyDiagram-EHGQE667-DqSSn_TX.js → wardleyDiagram-EHGQE667-BksgQ49c.js} +1 -1
  59. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-B5GDvD8r.js → xychartDiagram-FW5EYKEG-C4FbK9Sx.js} +1 -1
  60. package/dist/gui/index.html +2 -2
  61. package/package.json +1 -1
  62. package/dist/gui/assets/channel-DAAPjLkv.js +0 -1
  63. package/dist/gui/assets/classDiagram-OUVF2IWQ-DU6pj67q.js +0 -1
  64. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DU6pj67q.js +0 -1
  65. package/dist/gui/assets/index-B_NGA1xd.css +0 -1
  66. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-Ct4b-n_-.js +0 -1
  67. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-0XnzonY2.js +0 -8
package/dist/cli/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { homedir, networkInterfaces } from "node:os";
2
+ import { homedir, platform, networkInterfaces } from "node:os";
3
3
  import { Command, Option } from "commander";
4
- import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir, copyFile, appendFile } from "node:fs/promises";
4
+ import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir, appendFile, copyFile } from "node:fs/promises";
5
5
  import { join, dirname, extname, resolve, sep, isAbsolute, basename as basename$1, relative, normalize } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { spawn, execFile } from "node:child_process";
8
- import { existsSync, statSync, mkdirSync, watch, createReadStream, readFileSync } from "node:fs";
8
+ import { existsSync, statSync, mkdirSync, watch, createReadStream, openSync } from "node:fs";
9
9
  import { serve } from "@hono/node-server";
10
10
  import { Hono } from "hono";
11
11
  import { promisify } from "node:util";
@@ -21,7 +21,7 @@ import { Codex } from "@openai/codex-sdk";
21
21
  import { createOpencode } from "@opencode-ai/sdk";
22
22
  import { createInterface as createInterface$1 } from "node:readline/promises";
23
23
  import MarkdownIt from "markdown-it";
24
- const version = "0.3.5";
24
+ const version = "0.3.6";
25
25
  const pkg = {
26
26
  version
27
27
  };
@@ -1096,7 +1096,7 @@ function createSessionsRoutes(resolveProject) {
1096
1096
  });
1097
1097
  return app;
1098
1098
  }
1099
- const execFileP = promisify(execFile);
1099
+ const execFileP$1 = promisify(execFile);
1100
1100
  class GitError extends Error {
1101
1101
  code;
1102
1102
  stderr;
@@ -1151,7 +1151,7 @@ function assertSafeRelativePath(cwd, p) {
1151
1151
  }
1152
1152
  async function runGit(cwd, args) {
1153
1153
  try {
1154
- const result = await execFileP("git", args, {
1154
+ const result = await execFileP$1("git", args, {
1155
1155
  cwd,
1156
1156
  encoding: "utf8",
1157
1157
  maxBuffer: 16 * 1024 * 1024
@@ -1170,7 +1170,7 @@ async function runGitChecked(cwd, args) {
1170
1170
  }
1171
1171
  async function runGitRaw(cwd, args) {
1172
1172
  try {
1173
- const result = await execFileP("git", args, {
1173
+ const result = await execFileP$1("git", args, {
1174
1174
  cwd,
1175
1175
  encoding: "utf8",
1176
1176
  maxBuffer: 16 * 1024 * 1024
@@ -1463,6 +1463,365 @@ function buildGitOpsPrompt(action, specId, specRel, reviewRel) {
1463
1463
  `;
1464
1464
  }
1465
1465
  }
1466
+ const CURRENT_VERSION$1 = 1;
1467
+ const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
1468
+ const DEFAULT_NOTIFICATIONS = {
1469
+ sessionEnd: { banner: false, sound: false }
1470
+ };
1471
+ function resolveGlobalConfigDir(env = process.env) {
1472
+ if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
1473
+ const xdg = env.XDG_CONFIG_HOME?.trim();
1474
+ const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
1475
+ return join(base, "yorz");
1476
+ }
1477
+ function resolveGlobalConfigPath(env = process.env) {
1478
+ return join(resolveGlobalConfigDir(env), "projects.json");
1479
+ }
1480
+ async function loadGlobalConfig(filePath) {
1481
+ const fp = filePath ?? resolveGlobalConfigPath();
1482
+ if (!existsSync(fp)) return defaultGlobalConfig();
1483
+ let raw;
1484
+ try {
1485
+ raw = await readFile(fp, "utf8");
1486
+ } catch {
1487
+ return defaultGlobalConfig();
1488
+ }
1489
+ if (!raw.trim()) return defaultGlobalConfig();
1490
+ let parsed;
1491
+ try {
1492
+ parsed = JSON.parse(raw);
1493
+ } catch {
1494
+ return defaultGlobalConfig();
1495
+ }
1496
+ return normalizeConfig$1(parsed);
1497
+ }
1498
+ async function saveGlobalConfig(config, filePath) {
1499
+ const fp = filePath ?? resolveGlobalConfigPath();
1500
+ await mkdir(dirname(fp), { recursive: true });
1501
+ const normalized = normalizeConfig$1(config);
1502
+ const body = `${JSON.stringify(normalized, null, 2)}
1503
+ `;
1504
+ const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
1505
+ await writeFile(tmp, body, "utf8");
1506
+ await rename(tmp, fp);
1507
+ }
1508
+ function normalizeConfig$1(value) {
1509
+ if (!value || typeof value !== "object") return defaultGlobalConfig();
1510
+ const obj = value;
1511
+ const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
1512
+ const projects = [];
1513
+ for (const item of projectsRaw) {
1514
+ if (!item || typeof item !== "object") continue;
1515
+ const it = item;
1516
+ const id = typeof it.id === "string" ? it.id : "";
1517
+ const path = typeof it.path === "string" ? it.path : "";
1518
+ if (!id || !path) continue;
1519
+ const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
1520
+ const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
1521
+ const worktree = normalizeWorktree(it.worktree);
1522
+ const entry = { id, path, addedAt, lastActivityAt };
1523
+ if (worktree) entry.worktree = worktree;
1524
+ projects.push(entry);
1525
+ }
1526
+ return {
1527
+ version: CURRENT_VERSION$1,
1528
+ projects,
1529
+ agent: normalizeAgent$1(obj.agent),
1530
+ notifications: normalizeNotifications(obj.notifications)
1531
+ };
1532
+ }
1533
+ function defaultGlobalConfig() {
1534
+ return {
1535
+ version: CURRENT_VERSION$1,
1536
+ projects: [],
1537
+ agent: { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind },
1538
+ notifications: {
1539
+ sessionEnd: {
1540
+ banner: DEFAULT_NOTIFICATIONS.sessionEnd.banner,
1541
+ sound: DEFAULT_NOTIFICATIONS.sessionEnd.sound
1542
+ }
1543
+ }
1544
+ };
1545
+ }
1546
+ function normalizeAgent$1(value) {
1547
+ if (!value || typeof value !== "object") return { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind };
1548
+ const obj = value;
1549
+ const defaultKind = obj.defaultKind;
1550
+ if (defaultKind === "opencode" || defaultKind === "codex") return { defaultKind };
1551
+ return { defaultKind: "claude" };
1552
+ }
1553
+ function normalizeNotifications(value) {
1554
+ if (!value || typeof value !== "object") return defaultGlobalConfig().notifications;
1555
+ const obj = value;
1556
+ const sessionEndRaw = obj.sessionEnd;
1557
+ if (!sessionEndRaw || typeof sessionEndRaw !== "object")
1558
+ return defaultGlobalConfig().notifications;
1559
+ const sessionEndObj = sessionEndRaw;
1560
+ return {
1561
+ sessionEnd: {
1562
+ banner: typeof sessionEndObj.banner === "boolean" ? sessionEndObj.banner : DEFAULT_NOTIFICATIONS.sessionEnd.banner,
1563
+ sound: typeof sessionEndObj.sound === "boolean" ? sessionEndObj.sound : DEFAULT_NOTIFICATIONS.sessionEnd.sound
1564
+ }
1565
+ };
1566
+ }
1567
+ function normalizeWorktree(value) {
1568
+ if (!value || typeof value !== "object") return void 0;
1569
+ const it = value;
1570
+ const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
1571
+ const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
1572
+ const branch = typeof it.branch === "string" ? it.branch : "";
1573
+ const specId = typeof it.specId === "string" ? it.specId : "";
1574
+ const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
1575
+ if (!mainProjectId || !mainPath || !branch) return void 0;
1576
+ const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
1577
+ const meta = { mainProjectId, mainPath, branch, specId, createdAt };
1578
+ if (cleanSlug) meta.cleanSlug = cleanSlug;
1579
+ return meta;
1580
+ }
1581
+ function generateProjectId(absPath) {
1582
+ const base = basename$1(absPath);
1583
+ const slug = slugify(base) || "proj";
1584
+ const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
1585
+ return `${slug}-${hash}`;
1586
+ }
1587
+ function slugify(text) {
1588
+ return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
1589
+ }
1590
+ async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
1591
+ const config = await loadGlobalConfig(filePath);
1592
+ const existing = config.projects.find((p) => p.path === absPath);
1593
+ if (existing) return { entry: existing, created: false };
1594
+ const entry = {
1595
+ id: generateProjectId(absPath),
1596
+ path: absPath,
1597
+ addedAt: now().toISOString(),
1598
+ lastActivityAt: null
1599
+ };
1600
+ config.projects.push(entry);
1601
+ await saveGlobalConfig(config, filePath);
1602
+ return { entry, created: true };
1603
+ }
1604
+ async function setProjectWorktree(id, worktree, filePath) {
1605
+ const config = await loadGlobalConfig(filePath);
1606
+ const target = config.projects.find((p) => p.id === id);
1607
+ if (!target) return false;
1608
+ if (worktree) target.worktree = worktree;
1609
+ else delete target.worktree;
1610
+ await saveGlobalConfig(config, filePath);
1611
+ return true;
1612
+ }
1613
+ async function removeProject(id, filePath) {
1614
+ const config = await loadGlobalConfig(filePath);
1615
+ const before = config.projects.length;
1616
+ config.projects = config.projects.filter((p) => p.id !== id);
1617
+ if (config.projects.length === before) return false;
1618
+ await saveGlobalConfig(config, filePath);
1619
+ return true;
1620
+ }
1621
+ async function prepareProjectDir(input, cwd) {
1622
+ if (typeof input !== "string" || !input.trim()) {
1623
+ throw new Error("path required");
1624
+ }
1625
+ const trimmed = input.trim();
1626
+ const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
1627
+ if (!existsSync(abs)) {
1628
+ throw new Error(`path does not exist: ${abs}`);
1629
+ }
1630
+ const stats = await stat(abs);
1631
+ if (!stats.isDirectory()) {
1632
+ throw new Error(`path is not a directory: ${abs}`);
1633
+ }
1634
+ await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
1635
+ return abs;
1636
+ }
1637
+ const LEVEL_WEIGHT = {
1638
+ debug: 10,
1639
+ info: 20,
1640
+ warn: 30,
1641
+ error: 40
1642
+ };
1643
+ const LOG_LEVELS = Object.keys(LEVEL_WEIGHT);
1644
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
1645
+ const DEFAULT_MAX_ARCHIVES = 1;
1646
+ const DEFAULT_LOG_FILE = "serve.log";
1647
+ const STDIO_LOG_FILE = "serve-stdio.log";
1648
+ function resolveLogDir(env = process.env) {
1649
+ return join(resolveGlobalConfigDir(env), "logs");
1650
+ }
1651
+ function resolveLogLevel(env = process.env) {
1652
+ const raw = env.YORZ_LOG_LEVEL?.trim().toLowerCase();
1653
+ if (raw && LOG_LEVELS.includes(raw)) return raw;
1654
+ return "info";
1655
+ }
1656
+ function defaultOptions(env = process.env) {
1657
+ return {
1658
+ dir: resolveLogDir(env),
1659
+ fileName: DEFAULT_LOG_FILE,
1660
+ maxBytes: DEFAULT_MAX_BYTES,
1661
+ maxArchives: DEFAULT_MAX_ARCHIVES,
1662
+ level: resolveLogLevel(env),
1663
+ mirrorConsole: true
1664
+ };
1665
+ }
1666
+ class RotatingFileSink {
1667
+ constructor(dir, fileName, maxBytes, maxArchives) {
1668
+ this.dir = dir;
1669
+ this.fileName = fileName;
1670
+ this.maxBytes = maxBytes;
1671
+ this.maxArchives = maxArchives;
1672
+ }
1673
+ currentSize = 0;
1674
+ initialized = false;
1675
+ queue = Promise.resolve();
1676
+ get filePath() {
1677
+ return join(this.dir, this.fileName);
1678
+ }
1679
+ archivePath(index) {
1680
+ return `${this.filePath}.${index}`;
1681
+ }
1682
+ /** Enqueue a line; returns immediately, never throws. */
1683
+ write(line) {
1684
+ this.queue = this.queue.then(() => this.writeNow(line)).catch(() => {
1685
+ });
1686
+ }
1687
+ /** Resolve once every queued line has been flushed to disk. */
1688
+ async flush() {
1689
+ await this.queue.catch(() => {
1690
+ });
1691
+ }
1692
+ async writeNow(line) {
1693
+ try {
1694
+ await this.ensureInit();
1695
+ const bytes = Buffer.byteLength(line, "utf8");
1696
+ if (this.currentSize > 0 && this.currentSize + bytes > this.maxBytes) {
1697
+ await this.rotate();
1698
+ }
1699
+ await appendFile(this.filePath, line, "utf8");
1700
+ this.currentSize += bytes;
1701
+ } catch {
1702
+ }
1703
+ }
1704
+ /** Lazily create the dir and recover `currentSize` from an existing file. */
1705
+ async ensureInit() {
1706
+ if (this.initialized) return;
1707
+ await mkdir(this.dir, { recursive: true });
1708
+ try {
1709
+ const st = await stat(this.filePath);
1710
+ this.currentSize = st.size;
1711
+ } catch {
1712
+ this.currentSize = 0;
1713
+ }
1714
+ this.initialized = true;
1715
+ }
1716
+ async rotate() {
1717
+ if (this.maxArchives <= 0) {
1718
+ await writeFile(this.filePath, "", "utf8");
1719
+ this.currentSize = 0;
1720
+ return;
1721
+ }
1722
+ for (let i = this.maxArchives - 1; i >= 1; i--) {
1723
+ try {
1724
+ await rename(this.archivePath(i), this.archivePath(i + 1));
1725
+ } catch {
1726
+ }
1727
+ }
1728
+ await rename(this.filePath, this.archivePath(1));
1729
+ this.currentSize = 0;
1730
+ }
1731
+ }
1732
+ function createSink(options) {
1733
+ return new RotatingFileSink(options.dir, options.fileName, options.maxBytes, options.maxArchives);
1734
+ }
1735
+ function formatMeta(meta) {
1736
+ if (!meta) return "";
1737
+ const keys = Object.keys(meta);
1738
+ if (keys.length === 0) return "";
1739
+ try {
1740
+ return ` ${JSON.stringify(meta, jsonReplacer)}`;
1741
+ } catch {
1742
+ return "";
1743
+ }
1744
+ }
1745
+ function jsonReplacer(_key, value) {
1746
+ if (value instanceof Error) return { message: value.message, stack: value.stack };
1747
+ if (typeof value === "bigint") return value.toString();
1748
+ return value;
1749
+ }
1750
+ class Logger {
1751
+ constructor(core, scope) {
1752
+ this.core = core;
1753
+ this.scope = scope;
1754
+ }
1755
+ get level() {
1756
+ return this.core.options.level;
1757
+ }
1758
+ get filePath() {
1759
+ return this.core.sink.filePath;
1760
+ }
1761
+ get dir() {
1762
+ return this.core.options.dir;
1763
+ }
1764
+ /** Derive a logger that prefixes every line with `[scope]`. */
1765
+ child(scope) {
1766
+ const next = this.scope && this.scope !== "yorz" ? `${this.scope}:${scope}` : scope;
1767
+ return new Logger(this.core, next);
1768
+ }
1769
+ debug(msg, meta) {
1770
+ this.emit("debug", msg, meta);
1771
+ }
1772
+ info(msg, meta) {
1773
+ this.emit("info", msg, meta);
1774
+ }
1775
+ warn(msg, meta) {
1776
+ this.emit("warn", msg, meta);
1777
+ }
1778
+ error(msg, meta) {
1779
+ this.emit("error", msg, meta);
1780
+ }
1781
+ /** Replace options in place; every existing child logger picks the change up. */
1782
+ configure(patch) {
1783
+ const prev = this.core.options;
1784
+ const next = { ...prev, ...patch };
1785
+ this.core.options = next;
1786
+ const sinkChanged = next.dir !== prev.dir || next.fileName !== prev.fileName || next.maxBytes !== prev.maxBytes || next.maxArchives !== prev.maxArchives;
1787
+ if (sinkChanged) this.core.sink = createSink(next);
1788
+ }
1789
+ async flush() {
1790
+ await this.core.sink.flush();
1791
+ }
1792
+ emit(level, msg, meta) {
1793
+ const { options } = this.core;
1794
+ if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[options.level]) return;
1795
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] [${this.scope}] ${msg}${formatMeta(meta)}
1796
+ `;
1797
+ if (options.mirrorConsole) mirror(level, line);
1798
+ this.core.sink.write(line);
1799
+ }
1800
+ }
1801
+ function mirror(level, line) {
1802
+ const text = line.endsWith("\n") ? line.slice(0, -1) : line;
1803
+ try {
1804
+ if (level === "error") console.error(text);
1805
+ else if (level === "warn") console.warn(text);
1806
+ else console.log(text);
1807
+ } catch {
1808
+ }
1809
+ }
1810
+ function createLogger(patch = {}) {
1811
+ const options = { ...defaultOptions(), ...patch };
1812
+ return new Logger({ options, sink: createSink(options) }, "yorz");
1813
+ }
1814
+ let singleton = null;
1815
+ function getLogger() {
1816
+ if (!singleton) singleton = createLogger();
1817
+ return singleton;
1818
+ }
1819
+ function configureLogger(patch) {
1820
+ const logger = getLogger();
1821
+ logger.configure(patch);
1822
+ return logger;
1823
+ }
1824
+ const sseLog = () => getLogger().child("sse");
1466
1825
  const HEARTBEAT_INTERVAL_MS = 5e3;
1467
1826
  function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
1468
1827
  const timer = setInterval(() => {
@@ -1626,7 +1985,7 @@ class EventsHub {
1626
1985
  s.queue.push(frame);
1627
1986
  }
1628
1987
  } catch (err) {
1629
- console.error("[yorz] SSE emit error:", err);
1988
+ sseLog().warn("emit failed", { topic, err });
1630
1989
  }
1631
1990
  }
1632
1991
  async flush(s) {
@@ -1812,10 +2171,10 @@ function createProjectRoutes(registry, worktreeManager) {
1812
2171
  });
1813
2172
  return app;
1814
2173
  }
1815
- const CURRENT_VERSION$1 = 1;
2174
+ const CURRENT_VERSION = 1;
1816
2175
  const DEFAULT_SPECS_DIR = ".yorz/specs";
1817
2176
  function defaultProjectConfig() {
1818
- return { version: CURRENT_VERSION$1, agent: { kind: "claude" }, specsDir: DEFAULT_SPECS_DIR };
2177
+ return { version: CURRENT_VERSION, agent: { kind: "inherit" }, specsDir: DEFAULT_SPECS_DIR };
1819
2178
  }
1820
2179
  function configPath(cwd) {
1821
2180
  return join(cwd, ".yorz", "config.json");
@@ -1836,12 +2195,12 @@ async function loadProjectConfig(cwd) {
1836
2195
  } catch {
1837
2196
  return defaultProjectConfig();
1838
2197
  }
1839
- return normalizeConfig$1(parsed);
2198
+ return normalizeConfig(parsed);
1840
2199
  }
1841
2200
  async function saveProjectConfig(cwd, cfg) {
1842
2201
  const fp = configPath(cwd);
1843
2202
  await mkdir(dirname(fp), { recursive: true });
1844
- const normalized = normalizeConfig$1(cfg);
2203
+ const normalized = normalizeConfig(cfg);
1845
2204
  const body = `${JSON.stringify(normalized, null, 2)}
1846
2205
  `;
1847
2206
  const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
@@ -1861,12 +2220,12 @@ function resolveSpecsDir(cwd, cfg) {
1861
2220
  async function ensureSpecsDirExists(absSpecsDir) {
1862
2221
  await mkdir(absSpecsDir, { recursive: true });
1863
2222
  }
1864
- function normalizeConfig$1(value) {
2223
+ function normalizeConfig(value) {
1865
2224
  if (!value || typeof value !== "object") return defaultProjectConfig();
1866
2225
  const obj = value;
1867
2226
  const agent = normalizeAgent(obj.agent);
1868
2227
  const specsDir = normalizeSpecsDir(obj.specsDir);
1869
- return { version: CURRENT_VERSION$1, agent, specsDir };
2228
+ return { version: CURRENT_VERSION, agent, specsDir };
1870
2229
  }
1871
2230
  function normalizeAgent(value) {
1872
2231
  if (typeof value === "string") {
@@ -1877,6 +2236,7 @@ function normalizeAgent(value) {
1877
2236
  if (!value || typeof value !== "object") return { kind: "claude" };
1878
2237
  const obj = value;
1879
2238
  const kind = obj.kind;
2239
+ if (kind === "inherit") return { kind: "inherit" };
1880
2240
  if (kind === "opencode") return { kind: "opencode" };
1881
2241
  if (kind === "codex") return { kind: "codex" };
1882
2242
  if (kind === "custom") {
@@ -1912,7 +2272,7 @@ function createProjectConfigRoutes(registry) {
1912
2272
  } catch {
1913
2273
  return c.json({ error: "invalid JSON body" }, 400);
1914
2274
  }
1915
- const parsed = parseBody(raw);
2275
+ const parsed = parseBody$1(raw);
1916
2276
  if ("error" in parsed) return c.json({ error: parsed.error }, 400);
1917
2277
  const cfg = {
1918
2278
  version: 1,
@@ -1940,7 +2300,7 @@ function createProjectConfigRoutes(registry) {
1940
2300
  });
1941
2301
  return app;
1942
2302
  }
1943
- function parseBody(value) {
2303
+ function parseBody$1(value) {
1944
2304
  if (!value || typeof value !== "object") return { error: "body must be an object" };
1945
2305
  const obj = value;
1946
2306
  const agent = parseAgent(obj.agent);
@@ -1955,6 +2315,7 @@ function parseAgent(value) {
1955
2315
  }
1956
2316
  const obj = value;
1957
2317
  const kind = obj.kind;
2318
+ if (kind === "inherit") return { value: { kind: "inherit" } };
1958
2319
  if (kind === "claude") return { value: { kind: "claude" } };
1959
2320
  if (kind === "opencode") return { value: { kind: "opencode" } };
1960
2321
  if (kind === "codex") return { value: { kind: "codex" } };
@@ -1978,7 +2339,7 @@ function parseAgent(value) {
1978
2339
  }
1979
2340
  return { value: { kind: "custom", cmd: cmd.trim(), args } };
1980
2341
  }
1981
- return { error: "agent.kind must be claude | opencode | codex | custom" };
2342
+ return { error: "agent.kind must be inherit | claude | opencode | codex | custom" };
1982
2343
  }
1983
2344
  function parseSpecsDir(value) {
1984
2345
  const fallback = defaultProjectConfig().specsDir;
@@ -1992,6 +2353,62 @@ function parseSpecsDir(value) {
1992
2353
  }
1993
2354
  return { value: trimmed };
1994
2355
  }
2356
+ function createGlobalConfigRoutes(globalConfigPath) {
2357
+ const app = new Hono();
2358
+ app.get("/global-config", async (c) => {
2359
+ const cfg = await loadGlobalConfig(globalConfigPath);
2360
+ return c.json({ agent: cfg.agent, notifications: cfg.notifications });
2361
+ });
2362
+ app.put("/global-config", async (c) => {
2363
+ let raw;
2364
+ try {
2365
+ raw = await c.req.json();
2366
+ } catch {
2367
+ return c.json({ error: "invalid JSON body" }, 400);
2368
+ }
2369
+ const parsed = parseBody(raw);
2370
+ if ("error" in parsed) return c.json({ error: parsed.error }, 400);
2371
+ const cfg = await loadGlobalConfig(globalConfigPath);
2372
+ cfg.agent = parsed.agent;
2373
+ cfg.notifications = parsed.notifications;
2374
+ await saveGlobalConfig(cfg, globalConfigPath);
2375
+ return c.json({ ok: true, config: { agent: cfg.agent, notifications: cfg.notifications } });
2376
+ });
2377
+ return app;
2378
+ }
2379
+ function parseBody(value) {
2380
+ if (!value || typeof value !== "object") return { error: "body must be an object" };
2381
+ const obj = value;
2382
+ const agent = obj.agent;
2383
+ if (!agent || typeof agent !== "object") {
2384
+ return { error: "agent must be an object" };
2385
+ }
2386
+ const agentObj = agent;
2387
+ const defaultKind = agentObj.defaultKind;
2388
+ if (defaultKind !== "claude" && defaultKind !== "opencode" && defaultKind !== "codex") {
2389
+ return { error: "agent.defaultKind must be claude | opencode | codex" };
2390
+ }
2391
+ const notifications = obj.notifications;
2392
+ if (!notifications || typeof notifications !== "object") {
2393
+ return { error: "notifications must be an object" };
2394
+ }
2395
+ const n = notifications;
2396
+ const sessionEnd = n.sessionEnd;
2397
+ if (!sessionEnd || typeof sessionEnd !== "object") {
2398
+ return { error: "notifications.sessionEnd must be an object" };
2399
+ }
2400
+ const s = sessionEnd;
2401
+ if (typeof s.banner !== "boolean") {
2402
+ return { error: "notifications.sessionEnd.banner must be a boolean" };
2403
+ }
2404
+ if (typeof s.sound !== "boolean") {
2405
+ return { error: "notifications.sessionEnd.sound must be a boolean" };
2406
+ }
2407
+ return {
2408
+ agent: { defaultKind },
2409
+ notifications: { sessionEnd: { banner: s.banner, sound: s.sound } }
2410
+ };
2411
+ }
1995
2412
  function createSpecDraftsRoutes(resolveProject) {
1996
2413
  const app = new Hono();
1997
2414
  const need = async (c) => {
@@ -2393,6 +2810,7 @@ async function serveFile(c, file) {
2393
2810
  return c.body(body);
2394
2811
  }
2395
2812
  const DEBOUNCE_MS = 200;
2813
+ const registryLog = () => getLogger().child("registry");
2396
2814
  class RegistryEventBus {
2397
2815
  subs = /* @__PURE__ */ new Set();
2398
2816
  watcher = null;
@@ -2425,7 +2843,7 @@ class RegistryEventBus {
2425
2843
  this.scheduleEmit();
2426
2844
  });
2427
2845
  } catch (err) {
2428
- console.warn(`[registry-events] watch failed: ${err.message}`);
2846
+ registryLog().warn("projects.json watch failed", { dir, message: err.message });
2429
2847
  }
2430
2848
  }
2431
2849
  stop() {
@@ -2445,134 +2863,7 @@ class RegistryEventBus {
2445
2863
  this.debounceTimer.unref?.();
2446
2864
  }
2447
2865
  }
2448
- const CURRENT_VERSION = 1;
2449
- function resolveGlobalConfigDir(env = process.env) {
2450
- if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
2451
- const xdg = env.XDG_CONFIG_HOME?.trim();
2452
- const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
2453
- return join(base, "yorz");
2454
- }
2455
- function resolveGlobalConfigPath(env = process.env) {
2456
- return join(resolveGlobalConfigDir(env), "projects.json");
2457
- }
2458
- async function loadGlobalConfig(filePath) {
2459
- const fp = filePath ?? resolveGlobalConfigPath();
2460
- if (!existsSync(fp)) return { version: CURRENT_VERSION, projects: [] };
2461
- let raw;
2462
- try {
2463
- raw = await readFile(fp, "utf8");
2464
- } catch {
2465
- return { version: CURRENT_VERSION, projects: [] };
2466
- }
2467
- if (!raw.trim()) return { version: CURRENT_VERSION, projects: [] };
2468
- let parsed;
2469
- try {
2470
- parsed = JSON.parse(raw);
2471
- } catch {
2472
- return { version: CURRENT_VERSION, projects: [] };
2473
- }
2474
- return normalizeConfig(parsed);
2475
- }
2476
- async function saveGlobalConfig(config, filePath) {
2477
- const fp = filePath ?? resolveGlobalConfigPath();
2478
- await mkdir(dirname(fp), { recursive: true });
2479
- const normalized = normalizeConfig(config);
2480
- const body = `${JSON.stringify(normalized, null, 2)}
2481
- `;
2482
- const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
2483
- await writeFile(tmp, body, "utf8");
2484
- await rename(tmp, fp);
2485
- }
2486
- function normalizeConfig(value) {
2487
- if (!value || typeof value !== "object") return { version: CURRENT_VERSION, projects: [] };
2488
- const obj = value;
2489
- const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
2490
- const projects = [];
2491
- for (const item of projectsRaw) {
2492
- if (!item || typeof item !== "object") continue;
2493
- const it = item;
2494
- const id = typeof it.id === "string" ? it.id : "";
2495
- const path = typeof it.path === "string" ? it.path : "";
2496
- if (!id || !path) continue;
2497
- const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
2498
- const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
2499
- const worktree = normalizeWorktree(it.worktree);
2500
- const entry = { id, path, addedAt, lastActivityAt };
2501
- if (worktree) entry.worktree = worktree;
2502
- projects.push(entry);
2503
- }
2504
- return { version: CURRENT_VERSION, projects };
2505
- }
2506
- function normalizeWorktree(value) {
2507
- if (!value || typeof value !== "object") return void 0;
2508
- const it = value;
2509
- const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
2510
- const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
2511
- const branch = typeof it.branch === "string" ? it.branch : "";
2512
- const specId = typeof it.specId === "string" ? it.specId : "";
2513
- const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
2514
- if (!mainProjectId || !mainPath || !branch) return void 0;
2515
- const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
2516
- const meta = { mainProjectId, mainPath, branch, specId, createdAt };
2517
- if (cleanSlug) meta.cleanSlug = cleanSlug;
2518
- return meta;
2519
- }
2520
- function generateProjectId(absPath) {
2521
- const base = basename$1(absPath);
2522
- const slug = slugify(base) || "proj";
2523
- const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
2524
- return `${slug}-${hash}`;
2525
- }
2526
- function slugify(text) {
2527
- return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
2528
- }
2529
- async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
2530
- const config = await loadGlobalConfig(filePath);
2531
- const existing = config.projects.find((p) => p.path === absPath);
2532
- if (existing) return { entry: existing, created: false };
2533
- const entry = {
2534
- id: generateProjectId(absPath),
2535
- path: absPath,
2536
- addedAt: now().toISOString(),
2537
- lastActivityAt: null
2538
- };
2539
- config.projects.push(entry);
2540
- await saveGlobalConfig(config, filePath);
2541
- return { entry, created: true };
2542
- }
2543
- async function setProjectWorktree(id, worktree, filePath) {
2544
- const config = await loadGlobalConfig(filePath);
2545
- const target = config.projects.find((p) => p.id === id);
2546
- if (!target) return false;
2547
- if (worktree) target.worktree = worktree;
2548
- else delete target.worktree;
2549
- await saveGlobalConfig(config, filePath);
2550
- return true;
2551
- }
2552
- async function removeProject(id, filePath) {
2553
- const config = await loadGlobalConfig(filePath);
2554
- const before = config.projects.length;
2555
- config.projects = config.projects.filter((p) => p.id !== id);
2556
- if (config.projects.length === before) return false;
2557
- await saveGlobalConfig(config, filePath);
2558
- return true;
2559
- }
2560
- async function prepareProjectDir(input, cwd) {
2561
- if (typeof input !== "string" || !input.trim()) {
2562
- throw new Error("path required");
2563
- }
2564
- const trimmed = input.trim();
2565
- const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
2566
- if (!existsSync(abs)) {
2567
- throw new Error(`path does not exist: ${abs}`);
2568
- }
2569
- const stats = await stat(abs);
2570
- if (!stats.isDirectory()) {
2571
- throw new Error(`path is not a directory: ${abs}`);
2572
- }
2573
- await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
2574
- return abs;
2575
- }
2866
+ const worktreeLog = () => getLogger().child("worktree");
2576
2867
  class WorktreeManager {
2577
2868
  registry;
2578
2869
  globalConfigPath;
@@ -2620,6 +2911,13 @@ class WorktreeManager {
2620
2911
  };
2621
2912
  await setProjectWorktree(entry.id, meta, this.globalConfigPath);
2622
2913
  const updated = await this.registry.findEntry(entry.id) ?? { ...entry, worktree: meta };
2914
+ worktreeLog().info("worktree created", {
2915
+ mainProjectId: main.id,
2916
+ worktreeProjectId: entry.id,
2917
+ branch,
2918
+ path: wtPath,
2919
+ baseRef
2920
+ });
2623
2921
  return { entry: updated, branch, path: wtPath, baseRef };
2624
2922
  }
2625
2923
  /** Update the `specId` on a worktree entry once the spec has been created. */
@@ -2660,6 +2958,12 @@ class WorktreeManager {
2660
2958
  globalConfigPath: this.globalConfigPath ?? resolveGlobalConfigPath(),
2661
2959
  now: input.now ?? (() => /* @__PURE__ */ new Date())
2662
2960
  });
2961
+ worktreeLog().warn("merge back hit conflicts", {
2962
+ worktreeProjectId: entry.id,
2963
+ branch,
2964
+ mainPath,
2965
+ conflictSpecId: "conflictSpecId" in conflict ? conflict.conflictSpecId : void 0
2966
+ });
2663
2967
  return conflict;
2664
2968
  }
2665
2969
  const head = await runGitChecked(mainPath, ["rev-parse", "HEAD"]);
@@ -2674,6 +2978,12 @@ class WorktreeManager {
2674
2978
  await this.registry.reload(mainEntry.id);
2675
2979
  }
2676
2980
  this.onProjectsChanged?.();
2981
+ worktreeLog().info("worktree merged back", {
2982
+ worktreeProjectId: entry.id,
2983
+ mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
2984
+ branch,
2985
+ mergeCommit
2986
+ });
2677
2987
  return {
2678
2988
  status: "merged",
2679
2989
  mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
@@ -2701,6 +3011,7 @@ class WorktreeManager {
2701
3011
  }
2702
3012
  await this.registry.remove(worktreeProjectId);
2703
3013
  this.onProjectsChanged?.();
3014
+ worktreeLog().info("worktree removed", { worktreeProjectId, branch, path: wtPath });
2704
3015
  }
2705
3016
  async listWorktreesOf(mainProjectId) {
2706
3017
  const config = await loadGlobalConfig(this.globalConfigPath);
@@ -2711,7 +3022,7 @@ class WorktreeManager {
2711
3022
  const files = unmerged.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2712
3023
  const commitsByFile = {};
2713
3024
  for (const file of files) {
2714
- const log = await runGitRaw(mainPath, [
3025
+ const log2 = await runGitRaw(mainPath, [
2715
3026
  "log",
2716
3027
  "--since=30 days ago",
2717
3028
  "--pretty=format:%h%x09%ad%x09%an%x09%s",
@@ -2719,7 +3030,7 @@ class WorktreeManager {
2719
3030
  "--",
2720
3031
  file
2721
3032
  ]);
2722
- commitsByFile[file] = parseCommitLines(log.stdout);
3033
+ commitsByFile[file] = parseCommitLines(log2.stdout);
2723
3034
  }
2724
3035
  const merges = await runGitRaw(mainPath, [
2725
3036
  "log",
@@ -2767,9 +3078,10 @@ class WorktreeManager {
2767
3078
  if (this.triggerConflictAgent) {
2768
3079
  await this.triggerConflictAgent(args.mainProjectId, specId);
2769
3080
  } else {
2770
- console.warn(
2771
- `[worktree] triggerConflictAgent not configured; conflict spec ${specId} created at ${specPath} but no Agent was launched`
2772
- );
3081
+ worktreeLog().warn("triggerConflictAgent not configured; no Agent launched", {
3082
+ specId,
3083
+ specPath
3084
+ });
2773
3085
  }
2774
3086
  return {
2775
3087
  status: "conflict",
@@ -2916,8 +3228,25 @@ _暂无_
2916
3228
  function pad2(n) {
2917
3229
  return n < 10 ? `0${n}` : String(n);
2918
3230
  }
3231
+ const SLOW_REQUEST_MS = 1e3;
2919
3232
  function createApp(opts) {
2920
3233
  const app = new Hono();
3234
+ const httpLog = getLogger().child("http");
3235
+ const worktreeLog2 = getLogger().child("worktree");
3236
+ app.use("*", async (c, next) => {
3237
+ const startedAt = Date.now();
3238
+ await next();
3239
+ const durationMs = Date.now() - startedAt;
3240
+ const meta = {
3241
+ method: c.req.method,
3242
+ path: c.req.path,
3243
+ status: c.res.status,
3244
+ durationMs
3245
+ };
3246
+ if (c.res.status >= 400) httpLog.warn("request failed", meta);
3247
+ else if (durationMs >= SLOW_REQUEST_MS) httpLog.warn("slow request", meta);
3248
+ else httpLog.debug("request", meta);
3249
+ });
2921
3250
  const api = new Hono();
2922
3251
  const resolveProject = (id) => opts.registry.getOrCreate(id);
2923
3252
  const projectsBus = new RegistryEventBus();
@@ -2928,9 +3257,10 @@ function createApp(opts) {
2928
3257
  triggerConflictAgent: async (mainProjectId, specId) => {
2929
3258
  const main = await opts.registry.getOrCreate(mainProjectId);
2930
3259
  if (!main) {
2931
- console.warn(
2932
- `[worktree] cannot launch conflict Agent: main project ${mainProjectId} not resolvable`
2933
- );
3260
+ worktreeLog2.warn("cannot launch conflict Agent: main project not resolvable", {
3261
+ mainProjectId,
3262
+ specId
3263
+ });
2934
3264
  return;
2935
3265
  }
2936
3266
  const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
@@ -2941,6 +3271,7 @@ function createApp(opts) {
2941
3271
  }
2942
3272
  });
2943
3273
  api.route("/", createProjectRoutes(opts.registry, worktreeManager));
3274
+ api.route("/", createGlobalConfigRoutes(opts.registry.configPath()));
2944
3275
  api.route("/", createProjectConfigRoutes(opts.registry));
2945
3276
  api.route("/", createSpecsRoutes(resolveProject));
2946
3277
  api.route("/", createSessionsRoutes(resolveProject));
@@ -2952,7 +3283,13 @@ function createApp(opts) {
2952
3283
  app.route("/api", api);
2953
3284
  app.route("/", createStaticRoutes(opts.guiRoot));
2954
3285
  app.onError((err, c) => {
2955
- console.error("[yorz] route error:", err);
3286
+ httpLog.error("route error", {
3287
+ method: c.req.method,
3288
+ path: c.req.path,
3289
+ status: 500,
3290
+ message: err.message,
3291
+ stack: err.stack
3292
+ });
2956
3293
  return c.json({ error: "Internal Server Error", message: err.message }, 500);
2957
3294
  });
2958
3295
  return app;
@@ -3404,6 +3741,7 @@ function placeholderSummary(requirement) {
3404
3741
  if (!firstParagraph) return "(待 Agent 补全)";
3405
3742
  return firstParagraph.length > 200 ? `${firstParagraph.slice(0, 199)}…` : firstParagraph;
3406
3743
  }
3744
+ const watcherLog = () => getLogger().child("watcher");
3407
3745
  const UNLINK_SETTLE_MS = 80;
3408
3746
  class SpecWatcher {
3409
3747
  root;
@@ -3427,13 +3765,15 @@ class SpecWatcher {
3427
3765
  depth: 2,
3428
3766
  ...usePolling ? { usePolling: true, interval: 100 } : {}
3429
3767
  });
3430
- this.watcher.on("add", (p) => this.handle(p, "updated"));
3431
- this.watcher.on("change", (p) => this.handle(p, "updated"));
3432
- this.watcher.on("unlink", (p) => this.handle(p, "removed"));
3768
+ this.watcher.on("add", (p) => this.handle(p, "updated", "add"));
3769
+ this.watcher.on("change", (p) => this.handle(p, "updated", "change"));
3770
+ this.watcher.on("unlink", (p) => this.handle(p, "removed", "unlink"));
3771
+ this.watcher.on("error", (err) => watcherLog().warn("watch error", { root: this.root, err }));
3433
3772
  await new Promise((resolve2, reject) => {
3434
3773
  this.watcher.once("ready", () => resolve2());
3435
3774
  this.watcher.once("error", (err) => reject(err));
3436
3775
  });
3776
+ watcherLog().debug("watching specs dir", { root: this.root, usePolling });
3437
3777
  }
3438
3778
  async close() {
3439
3779
  for (const timer of this.pendingUnlinks.values()) clearTimeout(timer);
@@ -3462,10 +3802,11 @@ class SpecWatcher {
3462
3802
  this.listListeners.add(cb);
3463
3803
  return () => this.listListeners.delete(cb);
3464
3804
  }
3465
- async handle(filePath, evt) {
3805
+ async handle(filePath, evt, raw) {
3466
3806
  if (basename$1(filePath) !== "spec.md") return;
3467
3807
  const id = this.idForPath(filePath);
3468
3808
  if (!id) return;
3809
+ watcherLog().debug("spec file event", { event: raw, specId: id, path: filePath });
3469
3810
  if (evt === "removed") {
3470
3811
  this.scheduleUnlink(id, filePath);
3471
3812
  return;
@@ -4072,6 +4413,7 @@ class AdapterRegistry {
4072
4413
  this.byKind.clear();
4073
4414
  }
4074
4415
  }
4416
+ const agentLog = () => getLogger().child("agent");
4075
4417
  const SESSION_LIST_LIMIT = 30;
4076
4418
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4077
4419
  const TITLE_MAX_LENGTH = 64;
@@ -4086,10 +4428,11 @@ function summarizePromptForTitle(prompt) {
4086
4428
  return cleaned.length > TITLE_MAX_LENGTH ? `${cleaned.slice(0, TITLE_MAX_LENGTH - 1)}…` : cleaned;
4087
4429
  }
4088
4430
  class SessionManager {
4089
- constructor(cwd, defaultKind, store) {
4431
+ constructor(cwd, defaultKind, store, opts = {}) {
4090
4432
  this.cwd = cwd;
4091
4433
  this.defaultKind = defaultKind;
4092
4434
  this.store = store;
4435
+ this.opts = opts;
4093
4436
  this.adapters = new AdapterRegistry(cwd);
4094
4437
  this.statusEmitter.setMaxListeners(0);
4095
4438
  }
@@ -4215,9 +4558,11 @@ class SessionManager {
4215
4558
  const runId = randomUUID();
4216
4559
  const emitter = this.emitterFor(sid);
4217
4560
  let currentSid = sid;
4561
+ const startedAt = Date.now();
4218
4562
  await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
4219
4563
  });
4220
4564
  this.setRunning(sid, true);
4565
+ agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
4221
4566
  void (async () => {
4222
4567
  try {
4223
4568
  const ls = await this.ensureLive(sid);
@@ -4230,14 +4575,28 @@ class SessionManager {
4230
4575
  emitter.emit("event", ev);
4231
4576
  }
4232
4577
  } catch (err) {
4578
+ agentLog().error("dispatch failed", {
4579
+ sessionId: currentSid,
4580
+ runId,
4581
+ durationMs: Date.now() - startedAt,
4582
+ message: err instanceof Error ? err.message : String(err),
4583
+ stack: err instanceof Error ? err.stack : void 0
4584
+ });
4233
4585
  emitter.emit("event", {
4234
4586
  type: "error",
4235
4587
  message: err instanceof Error ? err.message : String(err)
4236
4588
  });
4237
4589
  } finally {
4590
+ agentLog().info("dispatch end", {
4591
+ sessionId: currentSid,
4592
+ runId,
4593
+ durationMs: Date.now() - startedAt
4594
+ });
4238
4595
  await this.store.touch(currentSid).catch(() => {
4239
4596
  });
4240
4597
  this.setRunning(currentSid, false);
4598
+ void Promise.resolve(this.opts.onSessionEnd?.(currentSid)).catch(() => {
4599
+ });
4241
4600
  emitter.emit("done");
4242
4601
  }
4243
4602
  })();
@@ -4261,6 +4620,7 @@ class SessionManager {
4261
4620
  return () => emitter.off("event", onEvent);
4262
4621
  }
4263
4622
  async reconcile(oldId, newId) {
4623
+ agentLog().debug("reconcile sessionId", { from: oldId, to: newId });
4264
4624
  const ls = this.live.get(oldId);
4265
4625
  if (ls) {
4266
4626
  this.live.delete(oldId);
@@ -4365,30 +4725,78 @@ class SessionStore {
4365
4725
  await this.persist();
4366
4726
  }
4367
4727
  }
4368
- function resolveAgentKind(cwd) {
4369
- const path = join(cwd, ".yorz", "config.json");
4370
- if (!existsSync(path)) return "claude";
4371
- let raw;
4372
- try {
4373
- raw = readFileSync(path, "utf8");
4374
- } catch {
4375
- return "claude";
4728
+ const execFileP = promisify(execFile);
4729
+ const COMMAND_TIMEOUT_MS = 4e3;
4730
+ function createSessionEndNotifier(opts = {}) {
4731
+ return async function notifySessionEnded() {
4732
+ const cfg = await loadGlobalConfig(opts.globalConfigPath);
4733
+ const sessionEnd = cfg.notifications.sessionEnd;
4734
+ if (!sessionEnd.banner && !sessionEnd.sound) return;
4735
+ const os = opts.platform ?? platform();
4736
+ const run = opts.runCommand ?? defaultRunCommand;
4737
+ const tasks = [];
4738
+ if (sessionEnd.banner) tasks.push(showBanner(os, run));
4739
+ if (sessionEnd.sound) tasks.push(playSound(os, run));
4740
+ await Promise.all(tasks.map((task) => task.catch(() => {
4741
+ })));
4742
+ };
4743
+ }
4744
+ async function defaultRunCommand(cmd, args) {
4745
+ await execFileP(cmd, args, {
4746
+ timeout: COMMAND_TIMEOUT_MS,
4747
+ windowsHide: true
4748
+ });
4749
+ }
4750
+ async function showBanner(os, run) {
4751
+ if (os === "darwin") {
4752
+ await run("osascript", ["-e", 'display notification "" with title "YorZ"']);
4753
+ return;
4376
4754
  }
4377
- if (!raw.trim()) return "claude";
4378
- let data;
4379
- try {
4380
- data = JSON.parse(raw);
4381
- } catch {
4382
- return "claude";
4755
+ if (os === "win32") {
4756
+ await run("powershell.exe", [
4757
+ "-NoProfile",
4758
+ "-NonInteractive",
4759
+ "-Command",
4760
+ [
4761
+ "Add-Type -AssemblyName System.Windows.Forms",
4762
+ "Add-Type -AssemblyName System.Drawing",
4763
+ "$n = New-Object System.Windows.Forms.NotifyIcon",
4764
+ "$n.Icon = [System.Drawing.SystemIcons]::Information",
4765
+ "$n.Visible = $true",
4766
+ "$n.ShowBalloonTip(5000, 'YorZ', '', [System.Windows.Forms.ToolTipIcon]::Info)",
4767
+ "Start-Sleep -Milliseconds 5500",
4768
+ "$n.Dispose()"
4769
+ ].join("; ")
4770
+ ]);
4771
+ return;
4383
4772
  }
4384
- if (!data || typeof data !== "object") return "claude";
4385
- const agent = data.agent;
4386
- if (typeof agent === "string") {
4387
- return agent === "codex" || agent === "opencode" ? agent : "claude";
4773
+ await run("notify-send", ["YorZ", ""]);
4774
+ }
4775
+ async function playSound(os, run) {
4776
+ if (os === "darwin") {
4777
+ await run("afplay", ["/System/Library/Sounds/Submarine.aiff"]);
4778
+ return;
4779
+ }
4780
+ if (os === "win32") {
4781
+ await run("powershell.exe", [
4782
+ "-NoProfile",
4783
+ "-NonInteractive",
4784
+ "-Command",
4785
+ "[console]::beep(440, 180)"
4786
+ ]);
4787
+ return;
4788
+ }
4789
+ for (const [cmd, args] of [
4790
+ ["paplay", ["/usr/share/sounds/freedesktop/stereo/complete.oga"]],
4791
+ ["canberra-gtk-play", ["-i", "complete"]],
4792
+ ["aplay", ["/usr/share/sounds/alsa/Front_Center.wav"]]
4793
+ ]) {
4794
+ try {
4795
+ await run(cmd, args);
4796
+ return;
4797
+ } catch {
4798
+ }
4388
4799
  }
4389
- if (!agent || typeof agent !== "object") return "claude";
4390
- const kind = agent.kind;
4391
- return kind === "codex" || kind === "opencode" ? kind : "claude";
4392
4800
  }
4393
4801
  class ProjectRegistry {
4394
4802
  globalConfigPath;
@@ -4488,6 +4896,7 @@ class ProjectRegistry {
4488
4896
  }
4489
4897
  async materialize(input) {
4490
4898
  const cfg = await loadProjectConfig(input.path);
4899
+ const globalCfg = await loadGlobalConfig(this.globalConfigPath);
4491
4900
  const specsDir = resolveSpecsDir(input.path, cfg);
4492
4901
  const specsDirRelative = posixRelative(input.path, specsDir);
4493
4902
  await ensureSpecsDirExists(specsDir);
@@ -4499,7 +4908,11 @@ class ProjectRegistry {
4499
4908
  });
4500
4909
  const attachments = new AttachmentStore({ cwd: input.path });
4501
4910
  const sessionStore = new SessionStore(input.path);
4502
- const sessions = new SessionManager(input.path, resolveAgentKind(input.path), sessionStore);
4911
+ const notifySessionEnded = createSessionEndNotifier({ globalConfigPath: this.globalConfigPath });
4912
+ const defaultKind = resolveProjectAgentKind(cfg.agent, globalCfg.agent.defaultKind);
4913
+ const sessions = new SessionManager(input.path, defaultKind, sessionStore, {
4914
+ onSessionEnd: notifySessionEnded
4915
+ });
4503
4916
  let closed = false;
4504
4917
  const instance = {
4505
4918
  id: input.id,
@@ -4530,6 +4943,11 @@ class ProjectRegistry {
4530
4943
  return instance;
4531
4944
  }
4532
4945
  }
4946
+ function resolveProjectAgentKind(projectAgent, globalDefault) {
4947
+ if (projectAgent.kind === "inherit") return globalDefault;
4948
+ if (projectAgent.kind === "codex" || projectAgent.kind === "opencode") return projectAgent.kind;
4949
+ return "claude";
4950
+ }
4533
4951
  function basename(p) {
4534
4952
  const parts = p.split(/[/\\]/).filter(Boolean);
4535
4953
  return parts[parts.length - 1] ?? p;
@@ -4556,6 +4974,7 @@ async function maxSpecUpdatedAt(projectPath) {
4556
4974
  }
4557
4975
  const DEFAULT_PORT = 7423;
4558
4976
  const MAX_PORT_TRIES = 10;
4977
+ const log$1 = () => getLogger().child("serve");
4559
4978
  async function start(opts = {}) {
4560
4979
  const registry = new ProjectRegistry({ globalConfigPath: opts.globalConfigPath });
4561
4980
  const cwd = opts.cwd ?? process.cwd();
@@ -4577,6 +4996,15 @@ async function start(opts = {}) {
4577
4996
  console.log(` - ${p.name} -> ${p.path}`);
4578
4997
  }
4579
4998
  console.log(`agent heartbeat enabled (interval=${HEARTBEAT_INTERVAL_MS / 1e3}s)`);
4999
+ log$1().info("service ready", {
5000
+ pid: process.pid,
5001
+ port: port.port,
5002
+ url,
5003
+ projects: projects.length,
5004
+ node: process.version,
5005
+ cli: pkg.version,
5006
+ logFile: getLogger().filePath
5007
+ });
4580
5008
  installGlobalErrorHandlers();
4581
5009
  if (opts.open) await tryOpenBrowser(url);
4582
5010
  return {
@@ -4618,10 +5046,18 @@ async function listen(fetchHandler, preferredPort) {
4618
5046
  );
4619
5047
  } catch (err) {
4620
5048
  lastErr = err;
4621
- if (err.code !== "EADDRINUSE") break;
5049
+ const code = err.code;
5050
+ if (code === "EADDRINUSE") {
5051
+ log$1().warn("port in use, retrying", { port: tryPort, attempt: i + 1, max: MAX_PORT_TRIES });
5052
+ } else {
5053
+ log$1().warn("listen failed", { port: tryPort, code, err: lastErr });
5054
+ break;
5055
+ }
4622
5056
  }
4623
5057
  }
4624
- throw lastErr ?? new Error(`failed to bind port near ${preferredPort}`);
5058
+ const fatal = lastErr ?? new Error(`failed to bind port near ${preferredPort}`);
5059
+ log$1().error("failed to bind any port", { preferredPort, tries: MAX_PORT_TRIES, err: fatal });
5060
+ throw fatal;
4625
5061
  }
4626
5062
  function listLanUrls(port) {
4627
5063
  const urls = [];
@@ -4643,23 +5079,9 @@ async function tryOpenBrowser(url) {
4643
5079
  } catch {
4644
5080
  }
4645
5081
  }
4646
- let errorLogReady = false;
4647
- async function ensureErrorLogDir() {
4648
- const dir = join(resolveGlobalConfigDir(), "logs");
4649
- if (!errorLogReady) {
4650
- await mkdir(dir, { recursive: true });
4651
- errorLogReady = true;
4652
- }
4653
- return join(dir, "serve-errors.log");
4654
- }
4655
5082
  function logCrash(kind, payload) {
4656
- const ts = (/* @__PURE__ */ new Date()).toISOString();
4657
5083
  const body = payload instanceof Error ? payload.stack ?? payload.message : String(payload);
4658
- const line = `[${ts}] [${kind}] ${body}
4659
- `;
4660
- console.error(`[yorz] ${kind}:`, payload);
4661
- void ensureErrorLogDir().then((fp) => appendFile(fp, line, "utf8")).catch(() => {
4662
- });
5084
+ log$1().error(kind, { detail: body });
4663
5085
  }
4664
5086
  function installGlobalErrorHandlers() {
4665
5087
  if (process.listenerCount("uncaughtException") > 0) return;
@@ -4670,10 +5092,25 @@ function installGlobalErrorHandlers() {
4670
5092
  logCrash("unhandledRejection", reason);
4671
5093
  });
4672
5094
  }
5095
+ const log = () => getLogger().child("serve");
5096
+ function silenceConsoleMirror() {
5097
+ configureLogger({ mirrorConsole: false });
5098
+ }
4673
5099
  const DEFAULT_SERVE_PORT = 7423;
4674
5100
  const RUNTIME_FILE = "runtime.json";
4675
5101
  const START_LOCK_DIR = "serve.lock";
4676
5102
  const START_WAIT_MS = 5e3;
5103
+ function backgroundStdio() {
5104
+ try {
5105
+ const dir = resolveLogDir();
5106
+ mkdirSync(dir, { recursive: true });
5107
+ const filePath = join(dir, STDIO_LOG_FILE);
5108
+ const fd = openSync(filePath, "w");
5109
+ return { stdio: ["ignore", fd, fd], path: filePath };
5110
+ } catch {
5111
+ return { stdio: "ignore", path: null };
5112
+ }
5113
+ }
4677
5114
  async function runServe(opts) {
4678
5115
  if (!opts.skipSkillCheck) {
4679
5116
  await ensureSkillsInstalledWithLog(opts.cwd ?? process.cwd());
@@ -4708,6 +5145,7 @@ async function runServe(opts) {
4708
5145
  return handle;
4709
5146
  }
4710
5147
  function startBackgroundServe(opts) {
5148
+ silenceConsoleMirror();
4711
5149
  return withStartLock(async () => {
4712
5150
  const live = await readLiveProcesses();
4713
5151
  if (live.length > 0) {
@@ -4725,12 +5163,24 @@ function startBackgroundServe(opts) {
4725
5163
  }
4726
5164
  const entry = process.argv[1];
4727
5165
  if (!entry) throw new Error("Cannot resolve CLI entrypoint for background service");
5166
+ const stdio = backgroundStdio();
4728
5167
  const child = spawn(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
4729
5168
  detached: true,
4730
- stdio: "ignore"
5169
+ stdio: stdio.stdio
4731
5170
  });
4732
5171
  child.unref();
5172
+ log().info("background service spawned", {
5173
+ pid: child.pid,
5174
+ stdioFile: stdio.path,
5175
+ logFile: getLogger().filePath
5176
+ });
4733
5177
  const runtime = await waitForRuntime(child.pid, START_WAIT_MS);
5178
+ if (!runtime) {
5179
+ log().warn("timed out waiting for runtime.json", {
5180
+ pid: child.pid,
5181
+ timeoutMs: START_WAIT_MS
5182
+ });
5183
+ }
4734
5184
  const port = runtime?.port ?? opts.port ?? DEFAULT_SERVE_PORT;
4735
5185
  const url = runtime?.url ?? `http://localhost:${port}/`;
4736
5186
  console.log(`YorZ Service started in background (pid=${child.pid ?? "unknown"}).`);
@@ -4766,6 +5216,7 @@ async function ensureSkillsInstalledWithLog(cwd) {
4766
5216
  }
4767
5217
  }
4768
5218
  async function runStopServe() {
5219
+ silenceConsoleMirror();
4769
5220
  const all = await readAllProcesses();
4770
5221
  if (all.length === 0) {
4771
5222
  return {
@@ -4779,6 +5230,7 @@ async function runStopServe() {
4779
5230
  const dead = all.filter((p) => !isProcessAlive(p.pid));
4780
5231
  if (alive.length === 0) {
4781
5232
  await removeRuntime();
5233
+ log().warn("cleared stale runtime records", { pids: dead.map((p) => p.pid) });
4782
5234
  const deadPidList = dead.map((p) => `pid=${p.pid}`).join(", ");
4783
5235
  return {
4784
5236
  stopped: false,
@@ -4793,12 +5245,15 @@ async function runStopServe() {
4793
5245
  for (const proc of alive) {
4794
5246
  try {
4795
5247
  process.kill(proc.pid, "SIGTERM");
4796
- } catch {
5248
+ log().info("sent SIGTERM", { pid: proc.pid, url: proc.url });
5249
+ } catch (err) {
5250
+ log().warn("SIGTERM failed", { pid: proc.pid, err });
4797
5251
  failedPids.push(proc.pid);
4798
5252
  continue;
4799
5253
  }
4800
5254
  let stopped = await waitForProcessExit(proc.pid, 2e3);
4801
5255
  if (!stopped) {
5256
+ log().warn("still alive after SIGTERM, escalating to SIGKILL", { pid: proc.pid });
4802
5257
  try {
4803
5258
  process.kill(proc.pid, "SIGKILL");
4804
5259
  } catch {
@@ -4809,6 +5264,7 @@ async function runStopServe() {
4809
5264
  stoppedPids.push(proc.pid);
4810
5265
  stoppedUrls.push(proc.url);
4811
5266
  } else {
5267
+ log().warn("process did not exit", { pid: proc.pid });
4812
5268
  failedPids.push(proc.pid);
4813
5269
  }
4814
5270
  }