@yorz/cli 0.7.0 → 0.8.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 (66) hide show
  1. package/dist/cli/index.js +1082 -276
  2. package/dist/gui/assets/{abnfDiagram-VRR7QNED-pQ0yp01L.js → abnfDiagram-VRR7QNED-D1kFOejy.js} +1 -1
  3. package/dist/gui/assets/{arc-k5zxnVZT.js → arc--bTdICho.js} +1 -1
  4. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-DX-yKkN9.js → architectureDiagram-ZJ3FMSHR-C3DMgcAW.js} +1 -1
  5. package/dist/gui/assets/{blockDiagram-677ZJIJ3-CMoP3PmZ.js → blockDiagram-677ZJIJ3-BCtvFOy7.js} +1 -1
  6. package/dist/gui/assets/{c4Diagram-LMCZKHZV-CFAjMeqQ.js → c4Diagram-LMCZKHZV-CDw5fv3L.js} +1 -1
  7. package/dist/gui/assets/channel-Zv7U9soI.js +1 -0
  8. package/dist/gui/assets/{chunk-2Q5K7J3B-CSYRtuuw.js → chunk-2Q5K7J3B-DbJTSeCo.js} +1 -1
  9. package/dist/gui/assets/{chunk-32BRIVSS-BRohWK3g.js → chunk-32BRIVSS-BcF_P8AP.js} +1 -1
  10. package/dist/gui/assets/{chunk-5VM5RSS4-BANnogAt.js → chunk-5VM5RSS4-CYp4dWLx.js} +1 -1
  11. package/dist/gui/assets/{chunk-EX3LRPZG-BcVKPAuP.js → chunk-EX3LRPZG-BDzejC8h.js} +1 -1
  12. package/dist/gui/assets/{chunk-JWPE2WC7-D4KqJLpk.js → chunk-JWPE2WC7-Fnenn1Qc.js} +1 -1
  13. package/dist/gui/assets/{chunk-MOJQB5TN-CDFJAykP.js → chunk-MOJQB5TN-Cx7nt_sO.js} +1 -1
  14. package/dist/gui/assets/{chunk-RYQCIY6F-BJN0o293.js → chunk-RYQCIY6F-MQPZLynl.js} +1 -1
  15. package/dist/gui/assets/{chunk-V7JOEXUC-B3m1VhnM.js → chunk-V7JOEXUC-F5RsZN0n.js} +1 -1
  16. package/dist/gui/assets/{chunk-VR4S4FIN-BLYOZzRK.js → chunk-VR4S4FIN-CoOdAASP.js} +1 -1
  17. package/dist/gui/assets/{chunk-XXDRQBXY-s69CKrwP.js → chunk-XXDRQBXY-Dp3kfiet.js} +1 -1
  18. package/dist/gui/assets/classDiagram-OUVF2IWQ-DRHl_SaL.js +1 -0
  19. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DRHl_SaL.js +1 -0
  20. package/dist/gui/assets/{cose-bilkent-JH36ORCC-BrNJSX1S.js → cose-bilkent-JH36ORCC-PfqHyC7G.js} +1 -1
  21. package/dist/gui/assets/{cynefin-VYW2F7L2-i8qxCu_0.js → cynefin-VYW2F7L2-DerSwhBp.js} +1 -1
  22. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-dpueKRs1.js → cynefinDiagram-TSTJHNR4-BwakZjCU.js} +1 -1
  23. package/dist/gui/assets/{dagre-VKFMJZFB-fqwQB8CS.js → dagre-VKFMJZFB-Bqgor74s.js} +1 -1
  24. package/dist/gui/assets/{diagram-FQU43EPY-B99SBDqa.js → diagram-FQU43EPY-DgyWxpcU.js} +1 -1
  25. package/dist/gui/assets/{diagram-G47NLZAW-DF4Oz_Q6.js → diagram-G47NLZAW-DwvD_Up0.js} +1 -1
  26. package/dist/gui/assets/{diagram-NH7WQ7WH-BIaXJ_6B.js → diagram-NH7WQ7WH-DSimddOu.js} +1 -1
  27. package/dist/gui/assets/{diagram-OA4YK3LP-CJ4_-IYz.js → diagram-OA4YK3LP-CrBUd7XP.js} +1 -1
  28. package/dist/gui/assets/{diagram-WEI45ONY-LvUrzfuG.js → diagram-WEI45ONY-BWgcRZqn.js} +1 -1
  29. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-De5G_4vB.js → ebnfDiagram-CCIWWBDH-g9dEaUVe.js} +1 -1
  30. package/dist/gui/assets/{erDiagram-Q63AITRT-rYLQVUAr.js → erDiagram-Q63AITRT-DhYVc7YD.js} +1 -1
  31. package/dist/gui/assets/{flowDiagram-23GEKE2U-CwDsoVsz.js → flowDiagram-23GEKE2U-COeJThES.js} +1 -1
  32. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-Bm1KWOcG.js → ganttDiagram-NO4QXBWP-DJxQzqsI.js} +1 -1
  33. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-s3jsEQWl.js → gitGraphDiagram-IHSO6WYX-CC_xIkVG.js} +1 -1
  34. package/dist/gui/assets/{index-Cft6LQWD.css → index-BzxGbxnX.css} +1 -1
  35. package/dist/gui/assets/index-tquwDQwL.js +39 -0
  36. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-xmq3a-E3.js → infoDiagram-FWYZ7A6U-C6FK6dRz.js} +1 -1
  37. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-CFPUA4LV.js → ishikawaDiagram-FXEZZL3T-D7b3GjN0.js} +1 -1
  38. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BOiMo2WC.js → journeyDiagram-5HDEW3XC-Ct0_MZXS.js} +1 -1
  39. package/dist/gui/assets/{kanban-definition-HUTT4EX6-o_oz2FbJ.js → kanban-definition-HUTT4EX6-M4nszR7h.js} +1 -1
  40. package/dist/gui/assets/{linear-D1YtFTY4.js → linear-CGUJayt8.js} +1 -1
  41. package/dist/gui/assets/{mermaid.core-DQyDucFo.js → mermaid.core-DpiXCPQI.js} +4 -4
  42. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-CX9Et0Qr.js → mindmap-definition-LN4V7U3C-CBbqzgM2.js} +1 -1
  43. package/dist/gui/assets/{pegDiagram-2B236MQR-DKoIr3v7.js → pegDiagram-2B236MQR-BW9OSYgR.js} +1 -1
  44. package/dist/gui/assets/{pieDiagram-ENE6RG2P-BSJigzmR.js → pieDiagram-ENE6RG2P-CT3MdE-x.js} +1 -1
  45. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Bpmh7Mn0.js → quadrantDiagram-ABIIQ3AL-BAqDSslO.js} +1 -1
  46. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-DoaeONQd.js → railroadDiagram-RFXS5EU6-BjxuOun7.js} +1 -1
  47. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-B7GlOm4X.js → requirementDiagram-TGXJPOKE-bqwQ3Xy2.js} +1 -1
  48. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-B7IwnQiq.js → sankeyDiagram-HTMAVEWB-D5UFHu3G.js} +1 -1
  49. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-Nwq2uhP9.js → sequenceDiagram-DBY2YBRQ-BimXCn08.js} +1 -1
  50. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-CCdTHNj_.js → sizeCapture-X5ZJPWSS-3iod82ny.js} +1 -1
  51. package/dist/gui/assets/{stateDiagram-2N3HPSRC-CqxRfIAb.js → stateDiagram-2N3HPSRC-BCkRUWaU.js} +1 -1
  52. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-7yy6idF2.js +1 -0
  53. package/dist/gui/assets/{swimlanes-5IMT3BWC-Bn-R9jtG.js → swimlanes-5IMT3BWC-D7XMYBp1.js} +2 -2
  54. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-DGa9rk3s.js +8 -0
  55. package/dist/gui/assets/{timeline-definition-FHXFAJF6-BKXgnjEG.js → timeline-definition-FHXFAJF6-D9U9I24L.js} +1 -1
  56. package/dist/gui/assets/{vennDiagram-L72KCM5P-Czw5UU3t.js → vennDiagram-L72KCM5P-DKs7jWNI.js} +1 -1
  57. package/dist/gui/assets/{wardleyDiagram-EHGQE667-BJS3BME3.js → wardleyDiagram-EHGQE667-CLvHSSW6.js} +1 -1
  58. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-DBt-6lUr.js → xychartDiagram-FW5EYKEG-V0U7x5OK.js} +1 -1
  59. package/dist/gui/index.html +2 -2
  60. package/package.json +2 -1
  61. package/dist/gui/assets/channel-sLqZREwh.js +0 -1
  62. package/dist/gui/assets/classDiagram-OUVF2IWQ-DpZPCbfk.js +0 -1
  63. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DpZPCbfk.js +0 -1
  64. package/dist/gui/assets/index-eECshTCa.js +0 -36
  65. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CWQWNOHt.js +0 -1
  66. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-D7kdlDFk.js +0 -8
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { homedir, platform } from "node:os";
3
3
  import { Command, Option } from "commander";
4
4
  import { stat, mkdir, writeFile, rename, readFile, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
5
- import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, sep, relative, normalize } from "node:path";
5
+ import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, relative, sep, normalize } from "node:path";
6
6
  import { createHash, randomUUID, randomBytes } from "node:crypto";
7
7
  import { existsSync, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
8
8
  import { spawn, execFile, execFileSync } from "node:child_process";
@@ -23,7 +23,7 @@ import { createOpencode } from "@opencode-ai/sdk";
23
23
  import { createInterface as createInterface$1 } from "node:readline/promises";
24
24
  import MarkdownIt from "markdown-it";
25
25
  const name = "@yorz/cli";
26
- const version = "0.7.0";
26
+ const version = "0.8.0";
27
27
  const type = "module";
28
28
  const description = "";
29
29
  const bin = { "yorz": "./dist/cli/index.js" };
@@ -35,7 +35,7 @@ const repository = { "type": "git", "url": "git+https://github.com/hughfenghen/Y
35
35
  const homepage = "https://github.com/hughfenghen/YorZ#readme";
36
36
  const publishConfig = { "access": "public" };
37
37
  const scripts = { "build": "pnpm run build:cli && pnpm run build:gui", "build:cli": "vite build", "build:gui": "vite build --config vite.gui.config.ts", "dev:cli": "pnpm run build:cli && node dist/cli/index.js serve --port 7424 --foreground", "dev:gui": "vite --config vite.gui.config.ts", "typecheck": "tsc -b", "test": "vitest run", "test:agent": "node scripts/test-agent.mjs", "test:e2e": "pnpm run build && playwright test", "format": 'prettier --write "src/**/*.{ts,tsx,md,css}"', "prepublishOnly": "pnpm run build" };
38
- const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
38
+ const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "parse-diff": "^0.12.0", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
39
39
  const devDependencies = { "@fontsource/jetbrains-mono": "^5.3.0", "@kobalte/core": "^0.13.12", "@playwright/test": "^1.61.0", "@solidjs/router": "^0.16.1", "@types/jsdom": "^21.1.7", "@types/markdown-it": "^14.1.2", "@types/node": "^22.10.0", "autoprefixer": "^10.5.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.1.0", "highlight.js": "^11.11.1", "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", "lucide-solid": "^1.24.0", "markdown-it-task-lists": "^2.1.1", "postcss": "^8.5.16", "prettier": "^3.4.0", "solid-js": "^1.9.13", "tailwind-merge": "^3.6.0", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.0", "vite": "^6.0.0", "vite-plugin-solid": "^2.11.12", "vitest": "^2.1.0" };
40
40
  const pkg = {
41
41
  name,
@@ -54,6 +54,93 @@ const pkg = {
54
54
  dependencies,
55
55
  devDependencies
56
56
  };
57
+ const HIDDEN_PROMPT_OPEN = "<!-- yorz:hidden -->";
58
+ const HIDDEN_PROMPT_CLOSE = "<!-- /yorz:hidden -->";
59
+ const SLASH_NAME_RE$1 = /^\/([\w-]+)(?:\s|$)/;
60
+ function matchCustomInstruction(prompt, instructions) {
61
+ const matched = SLASH_NAME_RE$1.exec(prompt.trim());
62
+ if (!matched) return null;
63
+ const name2 = matched[1];
64
+ return instructions.find((item) => item.name === name2) ?? null;
65
+ }
66
+ function wrapHiddenPrompt(hidden, original) {
67
+ const body = hidden.trim();
68
+ if (!body) return original;
69
+ return [HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE, original].join("\n");
70
+ }
71
+ function appendHiddenPrompt(original, hidden) {
72
+ const body = hidden.trim();
73
+ if (!body) return original;
74
+ return [original, "", HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE].join("\n");
75
+ }
76
+ function normalizeCustomInstructions(value) {
77
+ if (!Array.isArray(value)) return [];
78
+ const out = [];
79
+ const seen = /* @__PURE__ */ new Set();
80
+ for (const item of value) {
81
+ if (!item || typeof item !== "object") continue;
82
+ const obj = item;
83
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
84
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
85
+ if (!id || !name2 || seen.has(id) || !/^[\w-]+$/.test(name2)) continue;
86
+ seen.add(id);
87
+ out.push({
88
+ id,
89
+ name: name2,
90
+ description: typeof obj.description === "string" ? obj.description : "",
91
+ // Fall back to the pre-rename `systemPrompt` so existing configs keep
92
+ // working; the next save rewrites them under the new key.
93
+ hiddenPrompt: typeof obj.hiddenPrompt === "string" ? obj.hiddenPrompt : typeof obj.systemPrompt === "string" ? obj.systemPrompt : "",
94
+ prefill: typeof obj.prefill === "string" ? obj.prefill : "",
95
+ createdAt: typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0
96
+ });
97
+ }
98
+ return out;
99
+ }
100
+ function parseCustomInstructions(value) {
101
+ if (value === void 0) return [];
102
+ if (!Array.isArray(value)) return { error: "customInstructions must be an array" };
103
+ const out = [];
104
+ const seen = /* @__PURE__ */ new Set();
105
+ for (const [index, item] of value.entries()) {
106
+ if (!item || typeof item !== "object") {
107
+ return { error: `customInstructions.${index} must be an object` };
108
+ }
109
+ const obj = item;
110
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
111
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
112
+ if (!id) return { error: `customInstructions.${index}.id required` };
113
+ if (!name2 || !/^[\w-]+$/.test(name2)) {
114
+ return {
115
+ error: `customInstructions.${index}.name must use letters, numbers, underscores, or hyphens`
116
+ };
117
+ }
118
+ if (seen.has(id)) return { error: `duplicate custom instruction id: ${id}` };
119
+ seen.add(id);
120
+ const description2 = obj.description;
121
+ const hiddenPrompt = obj.hiddenPrompt ?? obj.systemPrompt;
122
+ const prefill = obj.prefill;
123
+ const createdAt = obj.createdAt;
124
+ if (typeof description2 !== "string") {
125
+ return { error: `customInstructions.${index}.description must be a string` };
126
+ }
127
+ if (typeof hiddenPrompt !== "string") {
128
+ return { error: `customInstructions.${index}.hiddenPrompt must be a string` };
129
+ }
130
+ if (typeof prefill !== "string") {
131
+ return { error: `customInstructions.${index}.prefill must be a string` };
132
+ }
133
+ if (typeof createdAt !== "number" || createdAt <= 0) {
134
+ return { error: `customInstructions.${index}.createdAt must be a positive number` };
135
+ }
136
+ out.push({ id, name: name2, description: description2, hiddenPrompt, prefill, createdAt });
137
+ }
138
+ return out;
139
+ }
140
+ function mergeCustomInstructions(project, global) {
141
+ const names = new Set(project.map((item) => item.name));
142
+ return [...project, ...global.filter((item) => !names.has(item.name))];
143
+ }
57
144
  const CURRENT_VERSION$1 = 1;
58
145
  const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
59
146
  const DEFAULT_NOTIFICATIONS = {
@@ -233,30 +320,6 @@ function normalizeAppearance(value) {
233
320
  language: isLanguage(language) ? language : DEFAULT_APPEARANCE.language
234
321
  };
235
322
  }
236
- function normalizeCustomInstructions(value) {
237
- if (!Array.isArray(value)) return [];
238
- const out = [];
239
- const seen = /* @__PURE__ */ new Set();
240
- for (const item of value) {
241
- if (!item || typeof item !== "object") continue;
242
- const obj = item;
243
- const id = typeof obj.id === "string" ? obj.id.trim() : "";
244
- const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
245
- if (!id || !name2 || seen.has(id) || !/^[\w-]+$/.test(name2)) continue;
246
- seen.add(id);
247
- out.push({
248
- id,
249
- name: name2,
250
- description: typeof obj.description === "string" ? obj.description : "",
251
- // Fall back to the pre-rename `systemPrompt` so existing configs keep
252
- // working; the next save rewrites them under the new key.
253
- hiddenPrompt: typeof obj.hiddenPrompt === "string" ? obj.hiddenPrompt : typeof obj.systemPrompt === "string" ? obj.systemPrompt : "",
254
- prefill: typeof obj.prefill === "string" ? obj.prefill : "",
255
- createdAt: typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0
256
- });
257
- }
258
- return out;
259
- }
260
323
  function isThemeMode(value) {
261
324
  return value === "system" || value === "light" || value === "dark";
262
325
  }
@@ -1136,6 +1199,202 @@ function skillEntryPath(skillName, env = process.env) {
1136
1199
  function skillRef(skillName, env = process.env) {
1137
1200
  return `请先完整阅读并严格遵循 ${skillEntryPath(skillName, env)}(YorZ 内置 ${skillName} skill 规则)`;
1138
1201
  }
1202
+ const METRICS_DIR_NAME = "metrics";
1203
+ const TELEMETRY_FILE_NAME = "telemetry.jsonl";
1204
+ const PROJECT_META_FILE_NAME = "project.json";
1205
+ const DEFAULT_TELEMETRY_MAX_BYTES = 5 * 1024 * 1024;
1206
+ const DEFAULT_TELEMETRY_MAX_ARCHIVES = 2;
1207
+ function resolveMetricsDir(env = process.env) {
1208
+ return join(resolveGlobalConfigDir(env), METRICS_DIR_NAME);
1209
+ }
1210
+ function resolveProjectMetricsDir(projectRoot, env = process.env) {
1211
+ return join(resolveMetricsDir(env), generateProjectId(resolve(projectRoot)));
1212
+ }
1213
+ function findProjectRoot(startDir) {
1214
+ let dir = resolve(startDir);
1215
+ for (; ; ) {
1216
+ if (existsSync(join(dir, ".yorz"))) return dir;
1217
+ const parent = dirname(dir);
1218
+ if (parent === dir) return null;
1219
+ dir = parent;
1220
+ }
1221
+ }
1222
+ function normalizeUsage(kind, raw) {
1223
+ if (!raw || typeof raw !== "object") return void 0;
1224
+ const obj = raw;
1225
+ const snapshot = kind === "codex" ? fromCodex(obj) : kind === "opencode" ? fromOpenCode(obj) : fromClaude(obj);
1226
+ return isEmpty(snapshot) ? void 0 : snapshot;
1227
+ }
1228
+ function fromClaude(u) {
1229
+ return compact({
1230
+ inputTokens: num$2(u.input_tokens),
1231
+ outputTokens: num$2(u.output_tokens),
1232
+ cacheReadTokens: num$2(u.cache_read_input_tokens),
1233
+ cacheCreateTokens: num$2(u.cache_creation_input_tokens)
1234
+ });
1235
+ }
1236
+ function fromCodex(u) {
1237
+ const total = num$2(u.input_tokens);
1238
+ const cached = num$2(u.cached_input_tokens);
1239
+ const uncached = total !== void 0 && cached !== void 0 ? Math.max(total - cached, 0) : total;
1240
+ return compact({
1241
+ inputTokens: uncached,
1242
+ cacheReadTokens: cached,
1243
+ outputTokens: num$2(u.output_tokens),
1244
+ reasoningTokens: num$2(u.reasoning_output_tokens)
1245
+ });
1246
+ }
1247
+ function fromOpenCode(u) {
1248
+ const cache = u.cache && typeof u.cache === "object" ? u.cache : {};
1249
+ return compact({
1250
+ inputTokens: num$2(u.input),
1251
+ outputTokens: num$2(u.output),
1252
+ reasoningTokens: num$2(u.reasoning),
1253
+ cacheReadTokens: num$2(cache.read),
1254
+ cacheCreateTokens: num$2(cache.write)
1255
+ });
1256
+ }
1257
+ function num$2(value) {
1258
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1259
+ }
1260
+ function compact(snapshot) {
1261
+ const out = {};
1262
+ for (const [key, value] of Object.entries(snapshot)) {
1263
+ if (value !== void 0) out[key] = value;
1264
+ }
1265
+ return out;
1266
+ }
1267
+ function isEmpty(snapshot) {
1268
+ return Object.keys(snapshot).length === 0;
1269
+ }
1270
+ const TELEMETRY_SCHEMA_VERSION = 1;
1271
+ function isTelemetryEnabled(env = process.env) {
1272
+ const raw = env.YORZ_TELEMETRY?.trim().toLowerCase();
1273
+ if (!raw) return true;
1274
+ return !["off", "0", "false", "no", "disabled"].includes(raw);
1275
+ }
1276
+ class TelemetryRecorder {
1277
+ constructor(projectRoot, projectId, dir, enabled) {
1278
+ this.projectRoot = projectRoot;
1279
+ this.projectId = projectId;
1280
+ this.dir = dir;
1281
+ this.enabled = enabled;
1282
+ this.sink = enabled ? new RotatingFileSink(
1283
+ dir,
1284
+ TELEMETRY_FILE_NAME,
1285
+ DEFAULT_TELEMETRY_MAX_BYTES,
1286
+ DEFAULT_TELEMETRY_MAX_ARCHIVES
1287
+ ) : null;
1288
+ }
1289
+ sink;
1290
+ metaWritten = false;
1291
+ get filePath() {
1292
+ return join(this.dir, TELEMETRY_FILE_NAME);
1293
+ }
1294
+ /** Queue one event. Returns immediately; never throws. */
1295
+ record(event, payload = {}) {
1296
+ if (!this.sink) return;
1297
+ let line;
1298
+ try {
1299
+ const envelope = {
1300
+ v: TELEMETRY_SCHEMA_VERSION,
1301
+ ts: nowStamp(),
1302
+ event,
1303
+ projectId: this.projectId,
1304
+ ...payload
1305
+ };
1306
+ line = `${JSON.stringify(envelope)}
1307
+ `;
1308
+ } catch {
1309
+ return;
1310
+ }
1311
+ this.sink.write(line);
1312
+ this.ensureMeta();
1313
+ }
1314
+ /** Resolve once every queued line has hit the disk (tests / shutdown). */
1315
+ async flush() {
1316
+ await this.sink?.flush();
1317
+ }
1318
+ /** Write the id → absolute path sidecar once per process. Best-effort. */
1319
+ ensureMeta() {
1320
+ if (this.metaWritten || !this.sink) return;
1321
+ this.metaWritten = true;
1322
+ const meta = {
1323
+ id: this.projectId,
1324
+ path: this.projectRoot,
1325
+ firstSeenAt: nowStamp()
1326
+ };
1327
+ void (async () => {
1328
+ try {
1329
+ await mkdir(this.dir, { recursive: true });
1330
+ await writeFile(join(this.dir, PROJECT_META_FILE_NAME), `${JSON.stringify(meta, null, 2)}
1331
+ `);
1332
+ } catch {
1333
+ }
1334
+ })();
1335
+ }
1336
+ }
1337
+ const recorders = /* @__PURE__ */ new Map();
1338
+ function getTelemetry(projectRoot, env = process.env) {
1339
+ const root = resolve(projectRoot);
1340
+ const cached = recorders.get(root);
1341
+ if (cached) return cached;
1342
+ const recorder = new TelemetryRecorder(
1343
+ root,
1344
+ generateProjectId(root),
1345
+ resolveProjectMetricsDir(root, env),
1346
+ isTelemetryEnabled(env)
1347
+ );
1348
+ recorders.set(root, recorder);
1349
+ return recorder;
1350
+ }
1351
+ function nowStamp(date = /* @__PURE__ */ new Date()) {
1352
+ const pad = (n) => String(n).padStart(2, "0");
1353
+ const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1354
+ return `${ymd} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
1355
+ }
1356
+ const TASK_LINE = /^- \[( |x|X)\]/gm;
1357
+ function snapshotSpec(detail) {
1358
+ if (!detail) return null;
1359
+ let tasksTotal = 0;
1360
+ let tasksDone = 0;
1361
+ for (const match of detail.body.matchAll(TASK_LINE)) {
1362
+ tasksTotal += 1;
1363
+ if (match[1] !== " ") tasksDone += 1;
1364
+ }
1365
+ return {
1366
+ stage: detail.frontmatter.stage,
1367
+ tasksTotal,
1368
+ tasksDone,
1369
+ specBytes: Buffer.byteLength(detail.body, "utf8")
1370
+ };
1371
+ }
1372
+ function trackSpecStage(opts) {
1373
+ const { projectRoot, store, specId, handle, before, trigger } = opts;
1374
+ const beforeSnapshot = snapshotSpec(before);
1375
+ handle.onDone(() => {
1376
+ void (async () => {
1377
+ let afterSnapshot = null;
1378
+ try {
1379
+ afterSnapshot = snapshotSpec(await store.read(specId));
1380
+ } catch {
1381
+ }
1382
+ const payload = {
1383
+ specId,
1384
+ trigger,
1385
+ traceId: handle.runId,
1386
+ sessionId: handle.sessionId,
1387
+ stageBefore: beforeSnapshot?.stage,
1388
+ stageAfter: afterSnapshot?.stage,
1389
+ tasksTotal: afterSnapshot?.tasksTotal,
1390
+ tasksDone: afterSnapshot?.tasksDone,
1391
+ tasksDoneBefore: beforeSnapshot?.tasksDone,
1392
+ specBytes: afterSnapshot?.specBytes
1393
+ };
1394
+ getTelemetry(projectRoot).record("spec.stage", payload);
1395
+ })();
1396
+ });
1397
+ }
1139
1398
  function createSpecsRoutes(resolveProject) {
1140
1399
  const app = new Hono();
1141
1400
  const need = async (c) => {
@@ -1166,7 +1425,7 @@ function createSpecsRoutes(resolveProject) {
1166
1425
  const beforeIds = new Set((await p.store.list()).map((s) => s.id));
1167
1426
  const prompt = buildDraftPrompt(input.type, input.requirement, input.draftId);
1168
1427
  const { sessionId } = await p.sessions.createSession();
1169
- const handle = await p.sessions.send(sessionId, prompt);
1428
+ const handle = await p.sessions.send(sessionId, prompt, void 0, { trigger: "new-spec" });
1170
1429
  handle.onDone((finalSessionId) => {
1171
1430
  void bindDraftSessionToCreatedSpec(p, beforeIds, finalSessionId);
1172
1431
  });
@@ -1302,7 +1561,18 @@ function createSpecsRoutes(resolveProject) {
1302
1561
  "resume",
1303
1562
  await buildDebugRuntimeContext(p)
1304
1563
  ) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
1305
- const handle = await p.sessions.send(sessionId, prompt);
1564
+ const handle = await p.sessions.send(sessionId, prompt, void 0, {
1565
+ trigger: "append",
1566
+ specId
1567
+ });
1568
+ trackSpecStage({
1569
+ projectRoot: p.path,
1570
+ store: p.store,
1571
+ specId,
1572
+ handle,
1573
+ before: detail,
1574
+ trigger: "append"
1575
+ });
1306
1576
  return c.json({ ok: true, runId: handle.runId, sessionId });
1307
1577
  }
1308
1578
  return c.json({ ok: true });
@@ -1316,7 +1586,18 @@ function createSpecsRoutes(resolveProject) {
1316
1586
  const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
1317
1587
  const debugActive = await readDebugMdStatus(join(p.specsDir, specId)) === "debugging";
1318
1588
  const prompt = debugActive ? buildDebugPrompt(p.specsDirRelative, specId, "resume", await buildDebugRuntimeContext(p)) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
1319
- const handle = await p.sessions.send(sessionId, prompt);
1589
+ const handle = await p.sessions.send(sessionId, prompt, void 0, {
1590
+ trigger: "run",
1591
+ specId
1592
+ });
1593
+ trackSpecStage({
1594
+ projectRoot: p.path,
1595
+ store: p.store,
1596
+ specId,
1597
+ handle,
1598
+ before: detail,
1599
+ trigger: "run"
1600
+ });
1320
1601
  return c.json({ runId: handle.runId, sessionId });
1321
1602
  });
1322
1603
  app.post("/projects/:projectId/specs/:id/explain", async (c) => {
@@ -1347,7 +1628,10 @@ ${text}
1347
1628
  """
1348
1629
  `;
1349
1630
  const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
1350
- const handle = await p.sessions.send(sessionId, prompt);
1631
+ const handle = await p.sessions.send(sessionId, prompt, void 0, {
1632
+ trigger: "explain",
1633
+ specId
1634
+ });
1351
1635
  return c.json({ runId: handle.runId, sessionId });
1352
1636
  });
1353
1637
  app.delete("/projects/:projectId/specs/:id", async (c) => {
@@ -1604,25 +1888,6 @@ function parseAnnotateBody(body) {
1604
1888
  if (note.length > 2e3) return { error: "note too long (max 2000)" };
1605
1889
  return { sectionPath, quote, note };
1606
1890
  }
1607
- const HIDDEN_PROMPT_OPEN = "<!-- yorz:hidden -->";
1608
- const HIDDEN_PROMPT_CLOSE = "<!-- /yorz:hidden -->";
1609
- const SLASH_NAME_RE$1 = /^\/([\w-]+)(?:\s|$)/;
1610
- function matchCustomInstruction(prompt, instructions) {
1611
- const matched = SLASH_NAME_RE$1.exec(prompt.trim());
1612
- if (!matched) return null;
1613
- const name2 = matched[1];
1614
- return instructions.find((item) => item.name === name2) ?? null;
1615
- }
1616
- function wrapHiddenPrompt(hidden, original) {
1617
- const body = hidden.trim();
1618
- if (!body) return original;
1619
- return [HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE, original].join("\n");
1620
- }
1621
- function appendHiddenPrompt(original, hidden) {
1622
- const body = hidden.trim();
1623
- if (!body) return original;
1624
- return [original, "", HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE].join("\n");
1625
- }
1626
1891
  const CHAT_DEBUG_DIR_REL = ".yorz/tmp/debug";
1627
1892
  const CHAT_DEBUG_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1628
1893
  const CHAT_DEBUG_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
@@ -1690,6 +1955,113 @@ function formatDebugTimestamp(date) {
1690
1955
  pad(date.getSeconds())
1691
1956
  ].join("");
1692
1957
  }
1958
+ const CURRENT_VERSION = 1;
1959
+ const DEFAULT_SPECS_DIR = ".yorz/specs";
1960
+ function defaultProjectConfig() {
1961
+ return {
1962
+ version: CURRENT_VERSION,
1963
+ agent: { kind: "inherit" },
1964
+ specsDir: DEFAULT_SPECS_DIR,
1965
+ commands: [],
1966
+ customInstructions: []
1967
+ };
1968
+ }
1969
+ function configPath(cwd) {
1970
+ return join(cwd, ".yorz", "config.json");
1971
+ }
1972
+ async function loadProjectConfig(cwd) {
1973
+ const fp = configPath(cwd);
1974
+ if (!existsSync(fp)) return defaultProjectConfig();
1975
+ let raw;
1976
+ try {
1977
+ raw = await readFile(fp, "utf8");
1978
+ } catch {
1979
+ return defaultProjectConfig();
1980
+ }
1981
+ if (!raw.trim()) return defaultProjectConfig();
1982
+ let parsed;
1983
+ try {
1984
+ parsed = JSON.parse(raw);
1985
+ } catch {
1986
+ return defaultProjectConfig();
1987
+ }
1988
+ return normalizeConfig(parsed);
1989
+ }
1990
+ async function saveProjectConfig(cwd, cfg) {
1991
+ const fp = configPath(cwd);
1992
+ await mkdir(dirname(fp), { recursive: true });
1993
+ const normalized = normalizeConfig(cfg);
1994
+ const body = `${JSON.stringify(normalized, null, 2)}
1995
+ `;
1996
+ const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
1997
+ await writeFile(tmp, body, "utf8");
1998
+ await rename(tmp, fp);
1999
+ }
2000
+ function resolveSpecsDir(cwd, cfg) {
2001
+ const root = resolve(cwd);
2002
+ const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
2003
+ const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
2004
+ const rel = relative(root, abs);
2005
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
2006
+ throw new Error(`specsDir must stay within the project root: ${raw}`);
2007
+ }
2008
+ return abs;
2009
+ }
2010
+ async function ensureSpecsDirExists(absSpecsDir) {
2011
+ await mkdir(absSpecsDir, { recursive: true });
2012
+ }
2013
+ function normalizeConfig(value) {
2014
+ if (!value || typeof value !== "object") return defaultProjectConfig();
2015
+ const obj = value;
2016
+ const agent = normalizeAgent(obj.agent);
2017
+ const specsDir = normalizeSpecsDir(obj.specsDir);
2018
+ const commands = normalizeCommands(obj.commands);
2019
+ const customInstructions = normalizeCustomInstructions(obj.customInstructions);
2020
+ return { version: CURRENT_VERSION, agent, specsDir, commands, customInstructions };
2021
+ }
2022
+ function normalizeCommands(value) {
2023
+ if (!Array.isArray(value)) return [];
2024
+ const out = [];
2025
+ const seen = /* @__PURE__ */ new Set();
2026
+ for (const item of value) {
2027
+ if (!item || typeof item !== "object") continue;
2028
+ const obj = item;
2029
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
2030
+ const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
2031
+ const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
2032
+ if (!id || !name2 || !cli || seen.has(id)) continue;
2033
+ seen.add(id);
2034
+ const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
2035
+ out.push({ id, name: name2, cli, createdAt });
2036
+ }
2037
+ return out;
2038
+ }
2039
+ function normalizeAgent(value) {
2040
+ if (typeof value === "string") {
2041
+ if (value === "opencode") return { kind: "opencode" };
2042
+ if (value === "codex") return { kind: "codex" };
2043
+ return { kind: "claude" };
2044
+ }
2045
+ if (!value || typeof value !== "object") return { kind: "claude" };
2046
+ const obj = value;
2047
+ const kind = obj.kind;
2048
+ if (kind === "inherit") return { kind: "inherit" };
2049
+ if (kind === "opencode") return { kind: "opencode" };
2050
+ if (kind === "codex") return { kind: "codex" };
2051
+ if (kind === "custom") {
2052
+ const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
2053
+ if (!cmd) return { kind: "claude" };
2054
+ const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
2055
+ return { kind: "custom", cmd, args };
2056
+ }
2057
+ return { kind: "claude" };
2058
+ }
2059
+ function normalizeSpecsDir(value) {
2060
+ if (typeof value !== "string") return DEFAULT_SPECS_DIR;
2061
+ const trimmed = value.trim();
2062
+ if (!trimmed) return DEFAULT_SPECS_DIR;
2063
+ return trimmed;
2064
+ }
1693
2065
  const SLASH_NAME_RE = /^\/([\w-]+)(?:\s|$)/;
1694
2066
  const SPEC_COMMAND_RE = /^\/yorz-spec(?:\s|$)/;
1695
2067
  function isSlashCommand(prompt) {
@@ -1739,6 +2111,13 @@ function resolveChatPrompt(prompt, instructions, opts = {}) {
1739
2111
  }
1740
2112
  return { prompt: buildUnknownCommandPrompt(original, hit), builtin: null };
1741
2113
  }
2114
+ async function loadCustomInstructions(projectPath) {
2115
+ const [project, global] = await Promise.all([
2116
+ loadProjectConfig(projectPath).then((cfg) => cfg.customInstructions).catch(() => []),
2117
+ loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => [])
2118
+ ]);
2119
+ return mergeCustomInstructions(project, global);
2120
+ }
1742
2121
  const KINDS = ["claude", "codex", "opencode"];
1743
2122
  const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
1744
2123
  function buildChatPrompt(prompt, draftId, attachments) {
@@ -1827,7 +2206,7 @@ function createSessionsRoutes(resolveProject) {
1827
2206
  }
1828
2207
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1829
2208
  if (!prompt) return c.json({ error: "prompt required" }, 400);
1830
- const instructions = isSlashCommand(prompt) ? await loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => []) : [];
2209
+ const instructions = isSlashCommand(prompt) ? await loadCustomInstructions(p.path) : [];
1831
2210
  const resolved = resolveChatPrompt(prompt, instructions, {
1832
2211
  specsDirRelative: p.specsDirRelative
1833
2212
  });
@@ -1844,7 +2223,9 @@ function createSessionsRoutes(resolveProject) {
1844
2223
  }
1845
2224
  if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
1846
2225
  });
1847
- const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt);
2226
+ const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt, {
2227
+ trigger: "chat"
2228
+ });
1848
2229
  return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
1849
2230
  });
1850
2231
  app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
@@ -1924,16 +2305,27 @@ function assertSafeRelativePath(cwd, p) {
1924
2305
  }
1925
2306
  return p;
1926
2307
  }
2308
+ function recordGitOp(cwd, args, startedAt, ok, code) {
2309
+ getTelemetry(cwd).record("git.op", {
2310
+ op: args[0] ?? "unknown",
2311
+ ok,
2312
+ exitCode: code,
2313
+ durMs: Date.now() - startedAt
2314
+ });
2315
+ }
1927
2316
  async function runGit(cwd, args) {
2317
+ const startedAt = Date.now();
1928
2318
  try {
1929
2319
  const result = await execFileWithoutWindow("git", args, {
1930
2320
  cwd,
1931
2321
  encoding: "utf8",
1932
2322
  maxBuffer: 16 * 1024 * 1024
1933
2323
  });
2324
+ recordGitOp(cwd, args, startedAt, true, 0);
1934
2325
  return { stdout: result.stdout, stderr: result.stderr };
1935
2326
  } catch (err) {
1936
2327
  const e = err;
2328
+ recordGitOp(cwd, args, startedAt, false, typeof e.code === "number" ? e.code : void 0);
1937
2329
  if (e.code === "ENOENT") {
1938
2330
  throw new GitError("git_missing", "git not found on PATH");
1939
2331
  }
@@ -1944,19 +2336,23 @@ async function runGitChecked(cwd, args) {
1944
2336
  return runGit(cwd, args);
1945
2337
  }
1946
2338
  async function runGitRaw(cwd, args) {
2339
+ const startedAt = Date.now();
1947
2340
  try {
1948
2341
  const result = await execFileWithoutWindow("git", args, {
1949
2342
  cwd,
1950
2343
  encoding: "utf8",
1951
2344
  maxBuffer: 16 * 1024 * 1024
1952
2345
  });
2346
+ recordGitOp(cwd, args, startedAt, true, 0);
1953
2347
  return { stdout: result.stdout, stderr: result.stderr, code: 0 };
1954
2348
  } catch (err) {
1955
2349
  const e = err;
1956
2350
  if (e.code === "ENOENT") {
2351
+ recordGitOp(cwd, args, startedAt, false);
1957
2352
  throw new GitError("git_missing", "git not found on PATH");
1958
2353
  }
1959
2354
  const code = typeof e.code === "number" ? e.code : 1;
2355
+ recordGitOp(cwd, args, startedAt, false, code);
1960
2356
  return { stdout: e.stdout ?? "", stderr: e.stderr ?? e.message ?? "", code };
1961
2357
  }
1962
2358
  }
@@ -2018,7 +2414,7 @@ async function commit(cwd, opts) {
2018
2414
  const extraPaths = renamed.map((r) => r.renamedFrom);
2019
2415
  const allPaths = [...opts.paths, ...extraPaths];
2020
2416
  await runGit(cwd, ["add", "--", ...allPaths]);
2021
- await runGit(cwd, ["commit", "-m", message, "--", ...allPaths]);
2417
+ await runGit(cwd, ["commit", "-m", message]);
2022
2418
  const { stdout } = await runGit(cwd, ["rev-parse", "HEAD"]);
2023
2419
  return { commit: stdout.trim() };
2024
2420
  }
@@ -2040,6 +2436,114 @@ async function discard(cwd, opts) {
2040
2436
  await runGit(cwd, ["clean", "-fd", "--", r.path]);
2041
2437
  }
2042
2438
  }
2439
+ const MAX_PATCH_BYTES = 512 * 1024;
2440
+ const MAX_PATCH_LINES = 3e3;
2441
+ function truncatePatch(patch) {
2442
+ let text = patch;
2443
+ let truncated = false;
2444
+ if (Buffer.byteLength(text, "utf8") > MAX_PATCH_BYTES) {
2445
+ text = text.slice(0, MAX_PATCH_BYTES);
2446
+ truncated = true;
2447
+ }
2448
+ const lines = text.split("\n");
2449
+ if (lines.length > MAX_PATCH_LINES) {
2450
+ text = lines.slice(0, MAX_PATCH_LINES).join("\n");
2451
+ truncated = true;
2452
+ }
2453
+ return { patch: text, truncated };
2454
+ }
2455
+ async function untrackedDiff(cwd, path) {
2456
+ let buf;
2457
+ try {
2458
+ buf = await readFile(resolve(cwd, path));
2459
+ } catch {
2460
+ return { path, patch: "", binary: false, truncated: false };
2461
+ }
2462
+ if (buf.subarray(0, 8e3).includes(0)) {
2463
+ return { path, patch: "", binary: true, truncated: false };
2464
+ }
2465
+ const text = buf.toString("utf8");
2466
+ const lines = text.length === 0 ? [] : text.replace(/\n$/, "").split("\n");
2467
+ const body = lines.map((l) => `+${l}`).join("\n");
2468
+ const header = `--- /dev/null
2469
+ +++ b/${path}
2470
+ @@ -0,0 +1,${lines.length} @@
2471
+ `;
2472
+ const { patch, truncated } = truncatePatch(lines.length ? `${header}${body}` : header);
2473
+ return { path, patch, binary: false, truncated };
2474
+ }
2475
+ async function fileDiff(cwd, path) {
2476
+ assertSafeRelativePath(cwd, path);
2477
+ const change = (await listChanges(cwd)).find((c) => c.path === path);
2478
+ if (change?.status === "??") return untrackedDiff(cwd, path);
2479
+ const pathArgs = change?.renamedFrom ? [change.renamedFrom, path] : [path];
2480
+ const res = await runGitRaw(cwd, ["diff", "HEAD", "--", ...pathArgs]);
2481
+ if (res.code !== 0 && !res.stdout) {
2482
+ throw new GitError("git_failed", res.stderr.trim() || "git diff failed", res.stderr);
2483
+ }
2484
+ const raw = res.stdout;
2485
+ if (!raw.includes("@@") && /^Binary files .* differ$/m.test(raw)) {
2486
+ return { path, patch: "", binary: true, truncated: false };
2487
+ }
2488
+ const { patch, truncated } = truncatePatch(raw);
2489
+ return { path, patch, binary: false, truncated };
2490
+ }
2491
+ async function currentBranch(cwd) {
2492
+ const { stdout } = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
2493
+ const branch = stdout.trim();
2494
+ if (!branch || branch === "HEAD") {
2495
+ throw new GitError("detached_head", "cannot operate from a detached HEAD");
2496
+ }
2497
+ return branch;
2498
+ }
2499
+ async function listBranches(cwd) {
2500
+ const [current, branchesResult] = await Promise.all([
2501
+ currentBranch(cwd),
2502
+ runGit(cwd, ["branch", "--format=%(refname:short)"])
2503
+ ]);
2504
+ const branches = branchesResult.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
2505
+ return { current, branches };
2506
+ }
2507
+ async function checkoutBranch(cwd, branch) {
2508
+ const target = branch?.trim() ?? "";
2509
+ if (!target) throw new GitError("invalid_branch", "branch must not be empty");
2510
+ const state = await listBranches(cwd);
2511
+ if (!state.branches.includes(target)) {
2512
+ throw new GitError("invalid_branch", `unknown local branch: ${target}`);
2513
+ }
2514
+ if (state.current === target) return { current: state.current };
2515
+ const res = await runGitRaw(cwd, ["checkout", target]);
2516
+ if (res.code !== 0) {
2517
+ throw new GitError("checkout_failed", res.stderr.trim() || "git checkout failed", res.stderr);
2518
+ }
2519
+ return { current: await currentBranch(cwd) };
2520
+ }
2521
+ async function push(cwd) {
2522
+ const branch = await currentBranch(cwd);
2523
+ const upstream = await runGitRaw(cwd, [
2524
+ "rev-parse",
2525
+ "--abbrev-ref",
2526
+ "--symbolic-full-name",
2527
+ "@{u}"
2528
+ ]);
2529
+ const hasUpstream = upstream.code === 0 && upstream.stdout.trim().length > 0;
2530
+ const args = hasUpstream ? ["push"] : ["push", "--set-upstream", "origin", branch];
2531
+ const res = await runGitRaw(cwd, args);
2532
+ if (res.code !== 0) {
2533
+ throw new GitError("push_failed", res.stderr.trim() || "git push failed", res.stderr);
2534
+ }
2535
+ return { branch, createdUpstream: !hasUpstream };
2536
+ }
2537
+ async function pull(cwd) {
2538
+ const branch = await currentBranch(cwd);
2539
+ const before = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
2540
+ const res = await runGitRaw(cwd, ["pull", "--ff-only"]);
2541
+ if (res.code !== 0) {
2542
+ throw new GitError("pull_failed", res.stderr.trim() || "git pull failed", res.stderr);
2543
+ }
2544
+ const after = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
2545
+ return { branch, updated: before !== after };
2546
+ }
2043
2547
  async function stash(cwd, opts) {
2044
2548
  if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
2045
2549
  throw new GitError("no_paths", "stash requires at least one path");
@@ -2080,7 +2584,18 @@ function createSpecReviewRoutes(resolveProject) {
2080
2584
  const specRel = `${p.specsDirRelative}/${specId}/spec.md`;
2081
2585
  const prompt = buildGitOpsPrompt(action, specId, specRel);
2082
2586
  const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
2083
- const handle = await p.sessions.send(sessionId, prompt);
2587
+ const handle = await p.sessions.send(sessionId, prompt, void 0, {
2588
+ trigger: "git-ops",
2589
+ specId
2590
+ });
2591
+ trackSpecStage({
2592
+ projectRoot: p.path,
2593
+ store: p.store,
2594
+ specId,
2595
+ handle,
2596
+ before: detail,
2597
+ trigger: "git-ops"
2598
+ });
2084
2599
  return c.json({ runId: handle.runId, sessionId });
2085
2600
  });
2086
2601
  app.get("/projects/:projectId/specs/:id/debug", async (c) => {
@@ -2098,68 +2613,6 @@ function createSpecReviewRoutes(resolveProject) {
2098
2613
  return c.json({ exists: false, text: "" });
2099
2614
  }
2100
2615
  });
2101
- app.get("/projects/:projectId/specs/:id/changes", async (c) => {
2102
- const p = await need(c);
2103
- if (p instanceof Response) return p;
2104
- const specId = c.req.param("id");
2105
- const detail = await p.store.read(specId);
2106
- if (!detail) return c.json({ error: "spec not found" }, 404);
2107
- try {
2108
- const changes = await listChanges(p.path);
2109
- return c.json({ changes });
2110
- } catch (err) {
2111
- if (err instanceof GitError) return c.json({ error: err.message }, 400);
2112
- throw err;
2113
- }
2114
- });
2115
- app.post("/projects/:projectId/specs/:id/commit", async (c) => {
2116
- const p = await need(c);
2117
- if (p instanceof Response) return p;
2118
- const specId = c.req.param("id");
2119
- const detail = await p.store.read(specId);
2120
- if (!detail) return c.json({ error: "spec not found" }, 404);
2121
- let body;
2122
- try {
2123
- body = await c.req.json();
2124
- } catch {
2125
- return c.json({ error: "invalid JSON body" }, 400);
2126
- }
2127
- const obj = body && typeof body === "object" ? body : {};
2128
- const message = typeof obj.message === "string" ? obj.message.trim() : "";
2129
- const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
2130
- if (!message) return c.json({ error: "message must not be empty" }, 400);
2131
- if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
2132
- try {
2133
- const result = await commit(p.path, { message, paths });
2134
- return c.json(result);
2135
- } catch (err) {
2136
- if (err instanceof GitError) return c.json({ error: err.message }, 400);
2137
- throw err;
2138
- }
2139
- });
2140
- app.post("/projects/:projectId/specs/:id/discard", async (c) => {
2141
- const p = await need(c);
2142
- if (p instanceof Response) return p;
2143
- const specId = c.req.param("id");
2144
- const detail = await p.store.read(specId);
2145
- if (!detail) return c.json({ error: "spec not found" }, 404);
2146
- let body;
2147
- try {
2148
- body = await c.req.json();
2149
- } catch {
2150
- return c.json({ error: "invalid JSON body" }, 400);
2151
- }
2152
- const obj = body && typeof body === "object" ? body : {};
2153
- const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
2154
- if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
2155
- try {
2156
- await discard(p.path, { paths });
2157
- return c.json({ ok: true });
2158
- } catch (err) {
2159
- if (err instanceof GitError) return c.json({ error: err.message }, 400);
2160
- throw err;
2161
- }
2162
- });
2163
2616
  app.post("/projects/:projectId/specs/:id/stash", async (c) => {
2164
2617
  const p = await need(c);
2165
2618
  if (p instanceof Response) return p;
@@ -2204,6 +2657,126 @@ function buildGitOpsPrompt(action, specId, specRel) {
2204
2657
  `;
2205
2658
  }
2206
2659
  }
2660
+ function createGitRoutes(resolveProject) {
2661
+ const app = new Hono();
2662
+ const need = async (c) => {
2663
+ const id = c.req.param("projectId") ?? "";
2664
+ if (!id) return c.json({ error: "projectId required" }, 400);
2665
+ const project = await resolveProject(id);
2666
+ if (!project) return c.json({ error: "project not found" }, 404);
2667
+ return project;
2668
+ };
2669
+ const readBody = async (c) => {
2670
+ try {
2671
+ const body = await c.req.json();
2672
+ return body && typeof body === "object" ? body : {};
2673
+ } catch {
2674
+ return null;
2675
+ }
2676
+ };
2677
+ const toPaths = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
2678
+ app.get("/projects/:projectId/git/changes", async (c) => {
2679
+ const p = await need(c);
2680
+ if (p instanceof Response) return p;
2681
+ try {
2682
+ return c.json({ changes: await listChanges(p.path) });
2683
+ } catch (err) {
2684
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2685
+ throw err;
2686
+ }
2687
+ });
2688
+ app.get("/projects/:projectId/git/diff", async (c) => {
2689
+ const p = await need(c);
2690
+ if (p instanceof Response) return p;
2691
+ const path = c.req.query("path") ?? "";
2692
+ if (!path) return c.json({ error: "path required" }, 400);
2693
+ try {
2694
+ return c.json(await fileDiff(p.path, path));
2695
+ } catch (err) {
2696
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2697
+ throw err;
2698
+ }
2699
+ });
2700
+ app.get("/projects/:projectId/git/branches", async (c) => {
2701
+ const p = await need(c);
2702
+ if (p instanceof Response) return p;
2703
+ try {
2704
+ return c.json(await listBranches(p.path));
2705
+ } catch (err) {
2706
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2707
+ throw err;
2708
+ }
2709
+ });
2710
+ app.post("/projects/:projectId/git/checkout", async (c) => {
2711
+ const p = await need(c);
2712
+ if (p instanceof Response) return p;
2713
+ const body = await readBody(c);
2714
+ if (!body) return c.json({ error: "invalid JSON body" }, 400);
2715
+ const branch = typeof body.branch === "string" ? body.branch.trim() : "";
2716
+ if (!branch) return c.json({ error: "branch must not be empty" }, 400);
2717
+ try {
2718
+ const result = await checkoutBranch(p.path, branch);
2719
+ return c.json({ ok: true, ...result });
2720
+ } catch (err) {
2721
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2722
+ throw err;
2723
+ }
2724
+ });
2725
+ app.post("/projects/:projectId/git/commit", async (c) => {
2726
+ const p = await need(c);
2727
+ if (p instanceof Response) return p;
2728
+ const body = await readBody(c);
2729
+ if (!body) return c.json({ error: "invalid JSON body" }, 400);
2730
+ const message = typeof body.message === "string" ? body.message.trim() : "";
2731
+ const paths = toPaths(body.paths);
2732
+ if (!message) return c.json({ error: "message must not be empty" }, 400);
2733
+ if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
2734
+ try {
2735
+ return c.json(await commit(p.path, { message, paths }));
2736
+ } catch (err) {
2737
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2738
+ throw err;
2739
+ }
2740
+ });
2741
+ app.post("/projects/:projectId/git/discard", async (c) => {
2742
+ const p = await need(c);
2743
+ if (p instanceof Response) return p;
2744
+ const body = await readBody(c);
2745
+ if (!body) return c.json({ error: "invalid JSON body" }, 400);
2746
+ const paths = toPaths(body.paths);
2747
+ if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
2748
+ try {
2749
+ await discard(p.path, { paths });
2750
+ return c.json({ ok: true });
2751
+ } catch (err) {
2752
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2753
+ throw err;
2754
+ }
2755
+ });
2756
+ app.post("/projects/:projectId/git/push", async (c) => {
2757
+ const p = await need(c);
2758
+ if (p instanceof Response) return p;
2759
+ try {
2760
+ const result = await push(p.path);
2761
+ return c.json({ ok: true, ...result });
2762
+ } catch (err) {
2763
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2764
+ throw err;
2765
+ }
2766
+ });
2767
+ app.post("/projects/:projectId/git/pull", async (c) => {
2768
+ const p = await need(c);
2769
+ if (p instanceof Response) return p;
2770
+ try {
2771
+ const result = await pull(p.path);
2772
+ return c.json({ ok: true, ...result });
2773
+ } catch (err) {
2774
+ if (err instanceof GitError) return c.json({ error: err.message }, 400);
2775
+ throw err;
2776
+ }
2777
+ });
2778
+ return app;
2779
+ }
2207
2780
  const sseLog = () => getLogger().child("sse");
2208
2781
  const HEARTBEAT_INTERVAL_MS = 5e3;
2209
2782
  function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
@@ -2395,9 +2968,8 @@ class EventsHub {
2395
2968
  if (rest === "commands") return this.attachCommandRuns(s, topic, project);
2396
2969
  const cm = /^command:(.+)$/.exec(rest);
2397
2970
  if (cm) return this.attachCommandRun(s, topic, project, cm[1]);
2398
- let sm = /^spec:([^:]+):changes$/.exec(rest);
2399
- if (sm) return this.attachSpecChanges(s, topic, project, sm[1]);
2400
- sm = /^spec:([^:]+)$/.exec(rest);
2971
+ if (rest === "changes") return this.attachProjectChanges(s, topic, project);
2972
+ let sm = /^spec:([^:]+)$/.exec(rest);
2401
2973
  if (sm) return this.attachSpec(s, topic, project, sm[1]);
2402
2974
  sm = /^session:(.+)$/.exec(rest);
2403
2975
  if (sm) return this.attachSession(s, topic, project, sm[1]);
@@ -2435,9 +3007,12 @@ class EventsHub {
2435
3007
  this.emit(s, topic, "updated", { type: kind, mtime });
2436
3008
  });
2437
3009
  }
2438
- async attachSpecChanges(s, topic, project, specId) {
2439
- const exists = await project.store.read(specId);
2440
- if (!exists) throw new Error("spec not found");
3010
+ /**
3011
+ * Git changes are repository-wide: the watcher is keyed on the project path,
3012
+ * so this topic carries no spec id and both the standalone Git page and the
3013
+ * spec Review page share one subscription.
3014
+ */
3015
+ attachProjectChanges(s, topic, project) {
2441
3016
  this.emit(s, topic, "ready", {});
2442
3017
  const { current, unsubscribe } = subscribeGitChanges(project.path, (changes) => {
2443
3018
  this.emit(s, topic, "changes-updated", { changes });
@@ -2593,111 +3168,6 @@ function createProjectRoutes(registry, worktreeManager) {
2593
3168
  });
2594
3169
  return app;
2595
3170
  }
2596
- const CURRENT_VERSION = 1;
2597
- const DEFAULT_SPECS_DIR = ".yorz/specs";
2598
- function defaultProjectConfig() {
2599
- return {
2600
- version: CURRENT_VERSION,
2601
- agent: { kind: "inherit" },
2602
- specsDir: DEFAULT_SPECS_DIR,
2603
- commands: []
2604
- };
2605
- }
2606
- function configPath(cwd) {
2607
- return join(cwd, ".yorz", "config.json");
2608
- }
2609
- async function loadProjectConfig(cwd) {
2610
- const fp = configPath(cwd);
2611
- if (!existsSync(fp)) return defaultProjectConfig();
2612
- let raw;
2613
- try {
2614
- raw = await readFile(fp, "utf8");
2615
- } catch {
2616
- return defaultProjectConfig();
2617
- }
2618
- if (!raw.trim()) return defaultProjectConfig();
2619
- let parsed;
2620
- try {
2621
- parsed = JSON.parse(raw);
2622
- } catch {
2623
- return defaultProjectConfig();
2624
- }
2625
- return normalizeConfig(parsed);
2626
- }
2627
- async function saveProjectConfig(cwd, cfg) {
2628
- const fp = configPath(cwd);
2629
- await mkdir(dirname(fp), { recursive: true });
2630
- const normalized = normalizeConfig(cfg);
2631
- const body = `${JSON.stringify(normalized, null, 2)}
2632
- `;
2633
- const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
2634
- await writeFile(tmp, body, "utf8");
2635
- await rename(tmp, fp);
2636
- }
2637
- function resolveSpecsDir(cwd, cfg) {
2638
- const root = resolve(cwd);
2639
- const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
2640
- const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
2641
- const rel = relative(root, abs);
2642
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
2643
- throw new Error(`specsDir must stay within the project root: ${raw}`);
2644
- }
2645
- return abs;
2646
- }
2647
- async function ensureSpecsDirExists(absSpecsDir) {
2648
- await mkdir(absSpecsDir, { recursive: true });
2649
- }
2650
- function normalizeConfig(value) {
2651
- if (!value || typeof value !== "object") return defaultProjectConfig();
2652
- const obj = value;
2653
- const agent = normalizeAgent(obj.agent);
2654
- const specsDir = normalizeSpecsDir(obj.specsDir);
2655
- const commands = normalizeCommands(obj.commands);
2656
- return { version: CURRENT_VERSION, agent, specsDir, commands };
2657
- }
2658
- function normalizeCommands(value) {
2659
- if (!Array.isArray(value)) return [];
2660
- const out = [];
2661
- const seen = /* @__PURE__ */ new Set();
2662
- for (const item of value) {
2663
- if (!item || typeof item !== "object") continue;
2664
- const obj = item;
2665
- const id = typeof obj.id === "string" ? obj.id.trim() : "";
2666
- const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
2667
- const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
2668
- if (!id || !name2 || !cli || seen.has(id)) continue;
2669
- seen.add(id);
2670
- const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
2671
- out.push({ id, name: name2, cli, createdAt });
2672
- }
2673
- return out;
2674
- }
2675
- function normalizeAgent(value) {
2676
- if (typeof value === "string") {
2677
- if (value === "opencode") return { kind: "opencode" };
2678
- if (value === "codex") return { kind: "codex" };
2679
- return { kind: "claude" };
2680
- }
2681
- if (!value || typeof value !== "object") return { kind: "claude" };
2682
- const obj = value;
2683
- const kind = obj.kind;
2684
- if (kind === "inherit") return { kind: "inherit" };
2685
- if (kind === "opencode") return { kind: "opencode" };
2686
- if (kind === "codex") return { kind: "codex" };
2687
- if (kind === "custom") {
2688
- const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
2689
- if (!cmd) return { kind: "claude" };
2690
- const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
2691
- return { kind: "custom", cmd, args };
2692
- }
2693
- return { kind: "claude" };
2694
- }
2695
- function normalizeSpecsDir(value) {
2696
- if (typeof value !== "string") return DEFAULT_SPECS_DIR;
2697
- const trimmed = value.trim();
2698
- if (!trimmed) return DEFAULT_SPECS_DIR;
2699
- return trimmed;
2700
- }
2701
3171
  function createProjectConfigRoutes(registry) {
2702
3172
  const app = new Hono();
2703
3173
  app.get("/projects/:projectId/config", async (c) => {
@@ -2724,7 +3194,8 @@ function createProjectConfigRoutes(registry) {
2724
3194
  version: 1,
2725
3195
  agent: parsed.agent,
2726
3196
  specsDir: parsed.specsDir,
2727
- commands: current.commands
3197
+ commands: current.commands,
3198
+ customInstructions: current.customInstructions
2728
3199
  };
2729
3200
  let absSpecsDir;
2730
3201
  try {
@@ -2745,6 +3216,34 @@ function createProjectConfigRoutes(registry) {
2745
3216
  await registry.reload(id);
2746
3217
  return c.json({ ok: true, config: cfg });
2747
3218
  });
3219
+ app.get("/projects/:projectId/custom-instructions", async (c) => {
3220
+ const id = c.req.param("projectId");
3221
+ const entry = await registry.findEntry(id);
3222
+ if (!entry) return c.json({ error: "project not found" }, 404);
3223
+ const cfg = await loadProjectConfig(entry.path);
3224
+ return c.json({ customInstructions: cfg.customInstructions });
3225
+ });
3226
+ app.put("/projects/:projectId/custom-instructions", async (c) => {
3227
+ const id = c.req.param("projectId");
3228
+ const entry = await registry.findEntry(id);
3229
+ if (!entry) return c.json({ error: "project not found" }, 404);
3230
+ let raw;
3231
+ try {
3232
+ raw = await c.req.json();
3233
+ } catch {
3234
+ return c.json({ error: "invalid JSON body" }, 400);
3235
+ }
3236
+ if (!raw || typeof raw !== "object") return c.json({ error: "body must be an object" }, 400);
3237
+ const parsed = parseCustomInstructions(raw.customInstructions);
3238
+ if ("error" in parsed) return c.json({ error: parsed.error }, 400);
3239
+ const current = await loadProjectConfig(entry.path);
3240
+ try {
3241
+ await saveProjectConfig(entry.path, { ...current, customInstructions: parsed });
3242
+ } catch (err) {
3243
+ return c.json({ error: err.message }, 500);
3244
+ }
3245
+ return c.json({ ok: true, customInstructions: parsed });
3246
+ });
2748
3247
  return app;
2749
3248
  }
2750
3249
  function parseBody$1(value) {
@@ -3062,46 +3561,6 @@ function parseAppearance(value) {
3062
3561
  if (!isLanguage(language)) return { error: "appearance.language must be zh-CN | en" };
3063
3562
  return { themeMode, themeName, language };
3064
3563
  }
3065
- function parseCustomInstructions(value) {
3066
- if (value === void 0) return [];
3067
- if (!Array.isArray(value)) return { error: "customInstructions must be an array" };
3068
- const out = [];
3069
- const seen = /* @__PURE__ */ new Set();
3070
- for (const [index, item] of value.entries()) {
3071
- if (!item || typeof item !== "object") {
3072
- return { error: `customInstructions.${index} must be an object` };
3073
- }
3074
- const obj = item;
3075
- const id = typeof obj.id === "string" ? obj.id.trim() : "";
3076
- const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
3077
- if (!id) return { error: `customInstructions.${index}.id required` };
3078
- if (!name2 || !/^[\w-]+$/.test(name2)) {
3079
- return {
3080
- error: `customInstructions.${index}.name must use letters, numbers, underscores, or hyphens`
3081
- };
3082
- }
3083
- if (seen.has(id)) return { error: `duplicate custom instruction id: ${id}` };
3084
- seen.add(id);
3085
- const description2 = obj.description;
3086
- const hiddenPrompt = obj.hiddenPrompt ?? obj.systemPrompt;
3087
- const prefill = obj.prefill;
3088
- const createdAt = obj.createdAt;
3089
- if (typeof description2 !== "string") {
3090
- return { error: `customInstructions.${index}.description must be a string` };
3091
- }
3092
- if (typeof hiddenPrompt !== "string") {
3093
- return { error: `customInstructions.${index}.hiddenPrompt must be a string` };
3094
- }
3095
- if (typeof prefill !== "string") {
3096
- return { error: `customInstructions.${index}.prefill must be a string` };
3097
- }
3098
- if (typeof createdAt !== "number" || createdAt <= 0) {
3099
- return { error: `customInstructions.${index}.createdAt must be a positive number` };
3100
- }
3101
- out.push({ id, name: name2, description: description2, hiddenPrompt, prefill, createdAt });
3102
- }
3103
- return out;
3104
- }
3105
3564
  function createSpecDraftsRoutes(resolveProject) {
3106
3565
  const app = new Hono();
3107
3566
  const need = async (c) => {
@@ -3944,6 +4403,15 @@ class CommandManager {
3944
4403
  this.notifyRuns();
3945
4404
  this.notifyRun(next);
3946
4405
  log$3().info("command ended", { runId, status, exitCode, signal });
4406
+ getTelemetry(this.projectPath).record("cmd.exec", {
4407
+ commandId: next.commandId,
4408
+ name: next.name,
4409
+ runId,
4410
+ status,
4411
+ exitCode: exitCode ?? void 0,
4412
+ signal: signal ?? void 0,
4413
+ durMs: (next.endedAt ?? Date.now()) - next.startedAt
4414
+ });
3947
4415
  }
3948
4416
  notifyRuns() {
3949
4417
  if (this.runsSubscribers.size === 0) return;
@@ -4766,7 +5234,7 @@ function parseCommitLines(stdout) {
4766
5234
  function renderConflictSpec(args) {
4767
5235
  const { branch, report, now } = args;
4768
5236
  const today = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
4769
- const nowStamp = `${today} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
5237
+ const nowStamp2 = `${today} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
4770
5238
  const fileLines = report.files.length ? report.files.map((f) => `- \`${f}\``).join("\n") : "- (无 — 合并失败但未列出冲突文件,请人工核查)";
4771
5239
  const commitBlocks = report.files.length ? report.files.map((f) => {
4772
5240
  const commits = report.commitsByFile[f] ?? [];
@@ -4836,7 +5304,7 @@ _暂无_
4836
5304
  "---",
4837
5305
  "stage: plan",
4838
5306
  "last_action: 由 worktree 合并冲突自动生成",
4839
- `updated_at: '${nowStamp}'`,
5307
+ `updated_at: '${nowStamp2}'`,
4840
5308
  `summary: 解决 ${branch} 合并到主项目时产生的 ${report.files.length} 个冲突文件`,
4841
5309
  "---",
4842
5310
  ""
@@ -4892,7 +5360,9 @@ function createApp(opts) {
4892
5360
  const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
4893
5361
  void main.sessions.send(
4894
5362
  sessionId,
4895
- `${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md`
5363
+ `${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md`,
5364
+ void 0,
5365
+ { trigger: "conflict", specId }
4896
5366
  );
4897
5367
  }
4898
5368
  });
@@ -4905,6 +5375,7 @@ function createApp(opts) {
4905
5375
  api.route("/", createSpecsRoutes(resolveProject));
4906
5376
  api.route("/", createSessionsRoutes(resolveProject));
4907
5377
  api.route("/", createSpecReviewRoutes(resolveProject));
5378
+ api.route("/", createGitRoutes(resolveProject));
4908
5379
  api.route("/", createSpecDraftsRoutes(resolveProject));
4909
5380
  api.route("/", createWorktreeRoutes(opts.registry, worktreeManager));
4910
5381
  api.route("/", createProjectFilesRoutes(resolveProject));
@@ -5378,6 +5849,7 @@ const watcherLog = () => getLogger().child("watcher");
5378
5849
  const UNLINK_SETTLE_MS = 80;
5379
5850
  class SpecWatcher {
5380
5851
  root;
5852
+ cwd;
5381
5853
  unlinkSettleMs;
5382
5854
  detailListeners = /* @__PURE__ */ new Map();
5383
5855
  listListeners = /* @__PURE__ */ new Set();
@@ -5388,6 +5860,7 @@ class SpecWatcher {
5388
5860
  watcher = null;
5389
5861
  constructor(opts) {
5390
5862
  this.root = opts.specsDir ?? join(opts.cwd, ".yorz", "specs");
5863
+ this.cwd = opts.cwd;
5391
5864
  this.unlinkSettleMs = opts.unlinkSettleMs ?? UNLINK_SETTLE_MS;
5392
5865
  }
5393
5866
  async start() {
@@ -5481,6 +5954,7 @@ class SpecWatcher {
5481
5954
  this.pendingUnlinks.set(id, timer);
5482
5955
  }
5483
5956
  emit(id, evt, mtimeMs) {
5957
+ getTelemetry(this.cwd).record("spec.change", { specId: id, kind: evt, mtimeMs });
5484
5958
  for (const cb of this.detailListeners.get(id) ?? []) cb(evt, mtimeMs);
5485
5959
  for (const cb of this.listListeners) cb();
5486
5960
  }
@@ -5544,6 +6018,29 @@ function usageWindows(usage) {
5544
6018
  }
5545
6019
  return out;
5546
6020
  }
6021
+ function num$1(value) {
6022
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
6023
+ }
6024
+ function isRecord(value) {
6025
+ return !!value && typeof value === "object" && !Array.isArray(value);
6026
+ }
6027
+ function withCost(usage, costUsd) {
6028
+ if (costUsd === void 0) return usage;
6029
+ return { ...usage ?? {}, costUsd };
6030
+ }
6031
+ function primaryModel(modelUsage) {
6032
+ if (!isRecord(modelUsage)) return void 0;
6033
+ let best;
6034
+ let bestOutput = -1;
6035
+ for (const [model, raw] of Object.entries(modelUsage)) {
6036
+ const output = isRecord(raw) ? num$1(raw.outputTokens) ?? 0 : 0;
6037
+ if (output > bestOutput) {
6038
+ bestOutput = output;
6039
+ best = model;
6040
+ }
6041
+ }
6042
+ return best;
6043
+ }
5547
6044
  class ClaudeSession {
5548
6045
  constructor(id, cwd, isNew) {
5549
6046
  this.id = id;
@@ -5570,6 +6067,7 @@ class ClaudeSession {
5570
6067
  try {
5571
6068
  const q = query({ prompt, options });
5572
6069
  let usage;
6070
+ let metrics;
5573
6071
  let completed = false;
5574
6072
  for await (const msg of q) {
5575
6073
  const m = msg;
@@ -5594,12 +6092,32 @@ class ClaudeSession {
5594
6092
  }
5595
6093
  } else if (m.type === "result") {
5596
6094
  usage = m.usage;
6095
+ metrics = {
6096
+ usage: withCost(normalizeUsage("claude", m.usage), num$1(m.total_cost_usd)),
6097
+ modelUsage: isRecord(m.modelUsage) ? m.modelUsage : void 0,
6098
+ numTurns: num$1(m.num_turns),
6099
+ stopReason: typeof m.stop_reason === "string" ? m.stop_reason : void 0,
6100
+ model: primaryModel(m.modelUsage),
6101
+ durationMs: num$1(m.duration_ms),
6102
+ apiDurationMs: num$1(m.duration_api_ms)
6103
+ };
6104
+ } else if (m.type === "system" && m.subtype === "compact_boundary") {
6105
+ const meta = m.compact_metadata ?? {};
6106
+ yield {
6107
+ type: "compact",
6108
+ metrics: {
6109
+ trigger: meta.trigger === "manual" || meta.trigger === "auto" ? meta.trigger : void 0,
6110
+ preTokens: num$1(meta.pre_tokens),
6111
+ postTokens: num$1(meta.post_tokens),
6112
+ durationMs: num$1(meta.duration_ms)
6113
+ }
6114
+ };
5597
6115
  } else if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
5598
6116
  completed = true;
5599
- yield { type: "turn-completed", usage };
6117
+ yield { type: "turn-completed", usage, metrics };
5600
6118
  }
5601
6119
  }
5602
- if (!completed) yield { type: "turn-completed", usage };
6120
+ if (!completed) yield { type: "turn-completed", usage, metrics };
5603
6121
  } catch (err) {
5604
6122
  if (ctrl.signal.aborted) return;
5605
6123
  yield { type: "error", message: err instanceof Error ? err.message : String(err) };
@@ -5882,7 +6400,11 @@ class CodexSession {
5882
6400
  yield { type: "error", message: item.message };
5883
6401
  }
5884
6402
  } else if (ev.type === "turn.completed") {
5885
- yield { type: "turn-completed", usage: ev.usage };
6403
+ yield {
6404
+ type: "turn-completed",
6405
+ usage: ev.usage,
6406
+ metrics: { usage: normalizeUsage("codex", ev.usage) }
6407
+ };
5886
6408
  } else if (ev.type === "turn.failed") {
5887
6409
  yield { type: "error", message: ev.error.message };
5888
6410
  } else if (ev.type === "error") {
@@ -6268,7 +6790,14 @@ class OpenCodeSession {
6268
6790
  for (const part of textParts(data.parts)) {
6269
6791
  if (part.type === "text") yield { type: "text", delta: part.text };
6270
6792
  }
6271
- yield { type: "turn-completed", usage: data.info.tokens };
6793
+ yield {
6794
+ type: "turn-completed",
6795
+ usage: data.info.tokens,
6796
+ metrics: {
6797
+ usage: normalizeUsage("opencode", data.info.tokens),
6798
+ model: typeof data.info.modelID === "string" ? data.info.modelID : void 0
6799
+ }
6800
+ };
6272
6801
  } catch (err) {
6273
6802
  if (this.aborted) return;
6274
6803
  yield { type: "error", message: err instanceof Error ? err.message : String(err) };
@@ -6636,9 +7165,11 @@ class SessionManager {
6636
7165
  * text while sending an expanded prompt to the Agent. Without it, an injected
6637
7166
  * hidden prompt would become the title.
6638
7167
  */
6639
- async send(sid, prompt, titleSource) {
7168
+ async send(sid, prompt, titleSource, meta = {}) {
6640
7169
  const runId = randomUUID();
6641
7170
  const emitter = this.emitterFor(sid);
7171
+ const telemetry = getTelemetry(this.cwd);
7172
+ const attribution = { specId: meta.specId, trigger: meta.trigger };
6642
7173
  let currentSid = sid;
6643
7174
  const startedAt = Date.now();
6644
7175
  await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
@@ -6647,18 +7178,57 @@ class SessionManager {
6647
7178
  });
6648
7179
  this.setRunning(sid, true);
6649
7180
  agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
7181
+ telemetry.record("agent.dispatch", {
7182
+ ...attribution,
7183
+ phase: "start",
7184
+ sessionId: sid,
7185
+ traceId: runId,
7186
+ promptLength: prompt.length
7187
+ });
7188
+ let agentKind;
7189
+ let dispatchError;
6650
7190
  const task = (async () => {
6651
7191
  try {
6652
7192
  const ls = await this.ensureLive(sid);
7193
+ agentKind = ls.kind;
6653
7194
  for await (const ev of ls.session.send(prompt)) {
6654
7195
  if (ev.type === "session-started" && ev.sessionId !== currentSid) {
6655
7196
  const oldSid = currentSid;
6656
7197
  currentSid = ev.sessionId;
6657
7198
  await this.reconcile(oldSid, ev.sessionId);
7199
+ } else if (ev.type === "turn-completed") {
7200
+ const m = ev.metrics ?? {};
7201
+ telemetry.record("agent.turn", {
7202
+ ...attribution,
7203
+ sessionId: currentSid,
7204
+ traceId: runId,
7205
+ agentKind,
7206
+ usage: m.usage,
7207
+ modelUsage: m.modelUsage,
7208
+ numTurns: m.numTurns,
7209
+ stopReason: m.stopReason,
7210
+ model: m.model,
7211
+ durMs: m.durationMs,
7212
+ apiDurMs: m.apiDurationMs
7213
+ });
7214
+ } else if (ev.type === "compact") {
7215
+ telemetry.record("agent.compact", {
7216
+ ...attribution,
7217
+ sessionId: currentSid,
7218
+ traceId: runId,
7219
+ agentKind,
7220
+ // Named apart from the dispatch `trigger` so both survive on the
7221
+ // same line: which action started the run, and what compacted it.
7222
+ compactTrigger: ev.metrics.trigger,
7223
+ preTokens: ev.metrics.preTokens,
7224
+ postTokens: ev.metrics.postTokens,
7225
+ durMs: ev.metrics.durationMs
7226
+ });
6658
7227
  }
6659
7228
  emitter.emit("event", ev);
6660
7229
  }
6661
7230
  } catch (err) {
7231
+ dispatchError = err instanceof Error ? err.message : String(err);
6662
7232
  agentLog().error("dispatch failed", {
6663
7233
  sessionId: currentSid,
6664
7234
  runId,
@@ -6676,6 +7246,17 @@ class SessionManager {
6676
7246
  runId,
6677
7247
  durationMs: Date.now() - startedAt
6678
7248
  });
7249
+ telemetry.record("agent.dispatch", {
7250
+ ...attribution,
7251
+ phase: "end",
7252
+ sessionId: currentSid,
7253
+ traceId: runId,
7254
+ agentKind,
7255
+ promptLength: prompt.length,
7256
+ durMs: Date.now() - startedAt,
7257
+ ok: dispatchError === void 0,
7258
+ errorMessage: dispatchError
7259
+ });
6679
7260
  await this.store.touch(currentSid).catch(() => {
6680
7261
  });
6681
7262
  this.setRunning(currentSid, false);
@@ -8867,6 +9448,7 @@ async function lintFile(path, opts = {}) {
8867
9448
  }
8868
9449
  [...new Set([...SPEC_RULES, ...REVIEW_RULES].map((r) => r.id))];
8869
9450
  async function runLint(opts) {
9451
+ const startedAt = Date.now();
8870
9452
  const cwd = resolve(opts.cwd);
8871
9453
  let targets = [];
8872
9454
  if (opts.all) {
@@ -8936,8 +9518,220 @@ async function runLint(opts) {
8936
9518
  Total: ${errorCount} error(s), ${warnCount} warn(s) across ${reports.length} file(s).
8937
9519
  `);
8938
9520
  }
9521
+ await recordLintRun(cwd, {
9522
+ fileCount: targets.length,
9523
+ errorCount,
9524
+ warnCount,
9525
+ exitCode,
9526
+ durMs: Date.now() - startedAt
9527
+ });
8939
9528
  return { reports, errorCount, warnCount, exitCode };
8940
9529
  }
9530
+ async function recordLintRun(cwd, payload) {
9531
+ const root = findProjectRoot(cwd);
9532
+ if (!root) return;
9533
+ const telemetry = getTelemetry(root);
9534
+ telemetry.record("lint.run", payload);
9535
+ await telemetry.flush();
9536
+ }
9537
+ const UNATTRIBUTED = "(no spec)";
9538
+ async function runMetrics(opts) {
9539
+ const dirs = await resolveTargetDirs(opts);
9540
+ if (dirs.length === 0) {
9541
+ process.stderr.write(
9542
+ "no telemetry found — run a dispatch first, or pass --project <id|path> / --all\n"
9543
+ );
9544
+ return { exitCode: 1, summary: emptySummary() };
9545
+ }
9546
+ const summary = emptySummary();
9547
+ for (const dir of dirs) {
9548
+ summary.projects.push(await projectLabel(dir));
9549
+ for (const file of await telemetryFiles(dir)) {
9550
+ summary.files.push(file);
9551
+ await consumeFile(file, opts.since, summary);
9552
+ }
9553
+ }
9554
+ summary.specs.sort((a, b) => b.costUsd - a.costUsd || b.turns - a.turns);
9555
+ if (opts.format === "json") {
9556
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}
9557
+ `);
9558
+ } else {
9559
+ process.stdout.write(renderText(summary));
9560
+ }
9561
+ return { exitCode: 0, summary };
9562
+ }
9563
+ async function resolveTargetDirs(opts) {
9564
+ const metricsDir = resolveMetricsDir();
9565
+ if (opts.all) {
9566
+ if (!existsSync(metricsDir)) return [];
9567
+ const entries = await readdir(metricsDir, { withFileTypes: true });
9568
+ return entries.filter((e) => e.isDirectory()).map((e) => join(metricsDir, e.name));
9569
+ }
9570
+ if (opts.project) {
9571
+ const dir2 = isAbsolute(opts.project) ? resolveProjectMetricsDir(opts.project) : join(metricsDir, opts.project);
9572
+ return existsSync(dir2) ? [dir2] : [];
9573
+ }
9574
+ const root = findProjectRoot(opts.cwd);
9575
+ if (!root) return [];
9576
+ const dir = resolveProjectMetricsDir(root);
9577
+ return existsSync(dir) ? [dir] : [];
9578
+ }
9579
+ async function telemetryFiles(dir) {
9580
+ if (!existsSync(dir)) return [];
9581
+ const entries = await readdir(dir);
9582
+ const archives = entries.filter((name2) => name2.startsWith(`${TELEMETRY_FILE_NAME}.`)).sort((a, b) => Number(b.split(".").pop()) - Number(a.split(".").pop())).map((name2) => join(dir, name2));
9583
+ const current = join(dir, TELEMETRY_FILE_NAME);
9584
+ return existsSync(current) ? [...archives, current] : archives;
9585
+ }
9586
+ async function projectLabel(dir) {
9587
+ try {
9588
+ const raw = await readFile(join(dir, PROJECT_META_FILE_NAME), "utf8");
9589
+ const meta = JSON.parse(raw);
9590
+ return meta.path ? `${meta.id ?? "?"} (${meta.path})` : meta.id ?? dir;
9591
+ } catch {
9592
+ return dir;
9593
+ }
9594
+ }
9595
+ async function consumeFile(file, since, out) {
9596
+ let raw;
9597
+ try {
9598
+ raw = await readFile(file, "utf8");
9599
+ } catch {
9600
+ return;
9601
+ }
9602
+ for (const line of raw.split("\n")) {
9603
+ if (!line.trim()) continue;
9604
+ let ev;
9605
+ try {
9606
+ ev = JSON.parse(line);
9607
+ } catch {
9608
+ out.skipped += 1;
9609
+ continue;
9610
+ }
9611
+ const ts = typeof ev.ts === "string" ? ev.ts : "";
9612
+ if (since && ts.slice(0, 10) < since) continue;
9613
+ out.lines += 1;
9614
+ const event = typeof ev.event === "string" ? ev.event : "unknown";
9615
+ out.eventCounts[event] = (out.eventCounts[event] ?? 0) + 1;
9616
+ applyEvent(ev, event, out);
9617
+ }
9618
+ }
9619
+ function applyEvent(ev, event, out) {
9620
+ if (!event.startsWith("agent.")) return;
9621
+ const bucket = bucketFor(out, typeof ev.specId === "string" ? ev.specId : UNATTRIBUTED);
9622
+ if (event === "agent.dispatch") {
9623
+ if (ev.phase !== "end") return;
9624
+ bucket.dispatches += 1;
9625
+ out.totals.dispatches += 1;
9626
+ if (ev.ok === false) {
9627
+ bucket.failures += 1;
9628
+ out.totals.failures += 1;
9629
+ }
9630
+ return;
9631
+ }
9632
+ if (event === "agent.compact") {
9633
+ bucket.compactions += 1;
9634
+ out.totals.compactions += 1;
9635
+ return;
9636
+ }
9637
+ if (event !== "agent.turn") return;
9638
+ bucket.turns += 1;
9639
+ out.totals.turns += 1;
9640
+ const durMs = num(ev.durMs);
9641
+ bucket.agentDurMs += durMs;
9642
+ out.totals.agentDurMs += durMs;
9643
+ const usage = ev.usage ?? {};
9644
+ for (const key of [
9645
+ "inputTokens",
9646
+ "cacheReadTokens",
9647
+ "cacheCreateTokens",
9648
+ "outputTokens"
9649
+ ]) {
9650
+ bucket[key] += num(usage[key]);
9651
+ out.totals[key] += num(usage[key]);
9652
+ }
9653
+ bucket.costUsd += num(usage.costUsd);
9654
+ out.totals.costUsd += num(usage.costUsd);
9655
+ }
9656
+ function bucketFor(out, specId) {
9657
+ let bucket = out.specs.find((s) => s.specId === specId);
9658
+ if (!bucket) {
9659
+ bucket = {
9660
+ specId,
9661
+ dispatches: 0,
9662
+ turns: 0,
9663
+ compactions: 0,
9664
+ inputTokens: 0,
9665
+ cacheReadTokens: 0,
9666
+ cacheCreateTokens: 0,
9667
+ outputTokens: 0,
9668
+ costUsd: 0,
9669
+ agentDurMs: 0,
9670
+ failures: 0,
9671
+ stages: []
9672
+ };
9673
+ out.specs.push(bucket);
9674
+ }
9675
+ return bucket;
9676
+ }
9677
+ function num(value) {
9678
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
9679
+ }
9680
+ function emptySummary() {
9681
+ return {
9682
+ projects: [],
9683
+ files: [],
9684
+ lines: 0,
9685
+ skipped: 0,
9686
+ eventCounts: {},
9687
+ totals: {
9688
+ dispatches: 0,
9689
+ turns: 0,
9690
+ compactions: 0,
9691
+ inputTokens: 0,
9692
+ cacheReadTokens: 0,
9693
+ cacheCreateTokens: 0,
9694
+ outputTokens: 0,
9695
+ costUsd: 0,
9696
+ agentDurMs: 0,
9697
+ failures: 0
9698
+ },
9699
+ specs: []
9700
+ };
9701
+ }
9702
+ const SPEC_ROW_LIMIT = 20;
9703
+ function renderText(s) {
9704
+ const lines = [];
9705
+ lines.push(`projects: ${s.projects.join(", ") || "(none)"}`);
9706
+ lines.push(`events: ${s.lines}${s.skipped ? ` (${s.skipped} unparseable line(s))` : ""}`);
9707
+ const kinds = Object.entries(s.eventCounts).sort((a, b) => b[1] - a[1]);
9708
+ if (kinds.length) {
9709
+ lines.push(`by kind: ${kinds.map(([k, n]) => `${k}=${n}`).join(" ")}`);
9710
+ }
9711
+ const t = s.totals;
9712
+ lines.push("");
9713
+ lines.push(
9714
+ `total: ${t.dispatches} dispatch(es), ${t.turns} turn(s), ${t.failures} failure(s), ${t.compactions} compaction(s)`
9715
+ );
9716
+ lines.push(
9717
+ `tokens: in=${t.inputTokens} cacheRead=${t.cacheReadTokens} cacheWrite=${t.cacheCreateTokens} out=${t.outputTokens}`
9718
+ );
9719
+ lines.push(`cost: $${t.costUsd.toFixed(4)} agent time: ${(t.agentDurMs / 1e3).toFixed(1)}s`);
9720
+ if (s.specs.length) {
9721
+ lines.push("");
9722
+ lines.push("per spec (by cost):");
9723
+ for (const spec of s.specs.slice(0, SPEC_ROW_LIMIT)) {
9724
+ lines.push(
9725
+ ` ${spec.specId.padEnd(40)} $${spec.costUsd.toFixed(4).padStart(9)} ${String(spec.dispatches).padStart(3)} disp ${String(spec.turns).padStart(3)} turn in=${spec.inputTokens} cacheRead=${spec.cacheReadTokens} out=${spec.outputTokens}`
9726
+ );
9727
+ }
9728
+ if (s.specs.length > SPEC_ROW_LIMIT) {
9729
+ lines.push(` … ${s.specs.length - SPEC_ROW_LIMIT} more spec(s) not shown (use --format json)`);
9730
+ }
9731
+ }
9732
+ return `${lines.join("\n")}
9733
+ `;
9734
+ }
8941
9735
  const program = new Command();
8942
9736
  program.name("yorz").description("YorZ CLI — manage the yorz-spec skill.").version(pkg.version);
8943
9737
  const uninstallCmd = program.command("uninstall").description("Remove YorZ artifacts (skills, ...).");
@@ -9001,6 +9795,18 @@ program.command("lint [paths...]").description("Lint spec.md / review.md files f
9001
9795
  if (result.exitCode !== 0) process.exit(result.exitCode);
9002
9796
  }
9003
9797
  );
9798
+ program.command("metrics").description("Summarize collected telemetry (token / cost / duration per spec).").option("--format <format>", "output format: text | json", "text").option("--project <idOrPath>", "project id or absolute path (default: project of --cwd)").option("--all", "aggregate across every project that has telemetry", false).option("--since <date>", "only events on or after this local date (YYYY-MM-DD)").option("--cwd <path>", "working directory (default: process.cwd())").action(
9799
+ async (opts) => {
9800
+ const result = await runMetrics({
9801
+ cwd: opts.cwd ?? process.cwd(),
9802
+ format: opts.format === "json" ? "json" : "text",
9803
+ project: opts.project,
9804
+ all: opts.all === true,
9805
+ since: opts.since
9806
+ });
9807
+ if (result.exitCode !== 0) process.exit(result.exitCode);
9808
+ }
9809
+ );
9004
9810
  const serveCmd = program.command("serve").description("Start the YorZ Service (HTTP + SSE + static GUI; multi-project).").option("-p, --port <port>", "port to listen on", "7423").option(
9005
9811
  "--host <address>",
9006
9812
  "bind address (default: 127.0.0.1; the service runs shell commands, so exposing it to the network is opt-in)"