@yorz/cli 0.7.1 → 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.
- package/dist/cli/index.js +846 -82
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-Bfi5VmJM.js → abnfDiagram-VRR7QNED-D1kFOejy.js} +1 -1
- package/dist/gui/assets/{arc-DW35HiMx.js → arc--bTdICho.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-D6ol2HjT.js → architectureDiagram-ZJ3FMSHR-C3DMgcAW.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-DimsChzs.js → blockDiagram-677ZJIJ3-BCtvFOy7.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-aQtOPEhO.js → c4Diagram-LMCZKHZV-CDw5fv3L.js} +1 -1
- package/dist/gui/assets/channel-Zv7U9soI.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-Bg7XMADz.js → chunk-2Q5K7J3B-DbJTSeCo.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-CJvj41Qg.js → chunk-32BRIVSS-BcF_P8AP.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-B6iQkE6V.js → chunk-5VM5RSS4-CYp4dWLx.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-Bime3fML.js → chunk-EX3LRPZG-BDzejC8h.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-B2d6pOPL.js → chunk-JWPE2WC7-Fnenn1Qc.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-C2BtAAqD.js → chunk-MOJQB5TN-Cx7nt_sO.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-DvOYjxsw.js → chunk-RYQCIY6F-MQPZLynl.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-HRF7SSaJ.js → chunk-V7JOEXUC-F5RsZN0n.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-oMVpMTS0.js → chunk-VR4S4FIN-CoOdAASP.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-Gx6kiEHQ.js → chunk-XXDRQBXY-Dp3kfiet.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DRHl_SaL.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DRHl_SaL.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-MoHCpl37.js → cose-bilkent-JH36ORCC-PfqHyC7G.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-2tnFSmNB.js → cynefin-VYW2F7L2-DerSwhBp.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-Cp7uDUjl.js → cynefinDiagram-TSTJHNR4-BwakZjCU.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-CTNuVp99.js → dagre-VKFMJZFB-Bqgor74s.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-Z9nTO-2q.js → diagram-FQU43EPY-DgyWxpcU.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-D1H3xA3x.js → diagram-G47NLZAW-DwvD_Up0.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-BGpM31nf.js → diagram-NH7WQ7WH-DSimddOu.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-Bhn6yNbV.js → diagram-OA4YK3LP-CrBUd7XP.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-BDJ6eaig.js → diagram-WEI45ONY-BWgcRZqn.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-C8UHOTF4.js → ebnfDiagram-CCIWWBDH-g9dEaUVe.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-BpoLARsO.js → erDiagram-Q63AITRT-DhYVc7YD.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-DSODAemd.js → flowDiagram-23GEKE2U-COeJThES.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-Bv5JA_dU.js → ganttDiagram-NO4QXBWP-DJxQzqsI.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-CW7ObzYy.js → gitGraphDiagram-IHSO6WYX-CC_xIkVG.js} +1 -1
- package/dist/gui/assets/{index-Cft6LQWD.css → index-BzxGbxnX.css} +1 -1
- package/dist/gui/assets/index-tquwDQwL.js +39 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-uTM_03X9.js → infoDiagram-FWYZ7A6U-C6FK6dRz.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-BlbCXOf8.js → ishikawaDiagram-FXEZZL3T-D7b3GjN0.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BwtfR_HU.js → journeyDiagram-5HDEW3XC-Ct0_MZXS.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-BXwvYv3u.js → kanban-definition-HUTT4EX6-M4nszR7h.js} +1 -1
- package/dist/gui/assets/{linear-C5c8PyN7.js → linear-CGUJayt8.js} +1 -1
- package/dist/gui/assets/{mermaid.core-Dbw2b69i.js → mermaid.core-DpiXCPQI.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-9P8fXZBY.js → mindmap-definition-LN4V7U3C-CBbqzgM2.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-81kUPbkr.js → pegDiagram-2B236MQR-BW9OSYgR.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-BI7iwgtu.js → pieDiagram-ENE6RG2P-CT3MdE-x.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-CSxA_Epi.js → quadrantDiagram-ABIIQ3AL-BAqDSslO.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-D4DnN2WS.js → railroadDiagram-RFXS5EU6-BjxuOun7.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-DjNONyxQ.js → requirementDiagram-TGXJPOKE-bqwQ3Xy2.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-Di4S5cUR.js → sankeyDiagram-HTMAVEWB-D5UFHu3G.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-CLVwUzez.js → sequenceDiagram-DBY2YBRQ-BimXCn08.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-C5y0Ry_b.js → sizeCapture-X5ZJPWSS-3iod82ny.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-IdHsn6JH.js → stateDiagram-2N3HPSRC-BCkRUWaU.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-7yy6idF2.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-bDCHEODt.js → swimlanes-5IMT3BWC-D7XMYBp1.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-DGa9rk3s.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-B6f6vq0E.js → timeline-definition-FHXFAJF6-D9U9I24L.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-AjYH8jaU.js → vennDiagram-L72KCM5P-DKs7jWNI.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-NV3g14vB.js → wardleyDiagram-EHGQE667-CLvHSSW6.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-D4yDWaI8.js → xychartDiagram-FW5EYKEG-V0U7x5OK.js} +1 -1
- package/dist/gui/index.html +2 -2
- package/package.json +2 -1
- package/dist/gui/assets/channel-CYD2fo3z.js +0 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-CFzbN5IF.js +0 -1
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CFzbN5IF.js +0 -1
- package/dist/gui/assets/index-CAnHTFea.js +0 -38
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CJBs-brH.js +0 -1
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BpY4RNvM.js +0 -8
package/dist/cli/index.js
CHANGED
|
@@ -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.
|
|
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,
|
|
@@ -1199,6 +1199,202 @@ function skillEntryPath(skillName, env = process.env) {
|
|
|
1199
1199
|
function skillRef(skillName, env = process.env) {
|
|
1200
1200
|
return `请先完整阅读并严格遵循 ${skillEntryPath(skillName, env)}(YorZ 内置 ${skillName} skill 规则)`;
|
|
1201
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
|
+
}
|
|
1202
1398
|
function createSpecsRoutes(resolveProject) {
|
|
1203
1399
|
const app = new Hono();
|
|
1204
1400
|
const need = async (c) => {
|
|
@@ -1229,7 +1425,7 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1229
1425
|
const beforeIds = new Set((await p.store.list()).map((s) => s.id));
|
|
1230
1426
|
const prompt = buildDraftPrompt(input.type, input.requirement, input.draftId);
|
|
1231
1427
|
const { sessionId } = await p.sessions.createSession();
|
|
1232
|
-
const handle = await p.sessions.send(sessionId, prompt);
|
|
1428
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, { trigger: "new-spec" });
|
|
1233
1429
|
handle.onDone((finalSessionId) => {
|
|
1234
1430
|
void bindDraftSessionToCreatedSpec(p, beforeIds, finalSessionId);
|
|
1235
1431
|
});
|
|
@@ -1365,7 +1561,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1365
1561
|
"resume",
|
|
1366
1562
|
await buildDebugRuntimeContext(p)
|
|
1367
1563
|
) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1368
|
-
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
|
+
});
|
|
1369
1576
|
return c.json({ ok: true, runId: handle.runId, sessionId });
|
|
1370
1577
|
}
|
|
1371
1578
|
return c.json({ ok: true });
|
|
@@ -1379,7 +1586,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1379
1586
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1380
1587
|
const debugActive = await readDebugMdStatus(join(p.specsDir, specId)) === "debugging";
|
|
1381
1588
|
const prompt = debugActive ? buildDebugPrompt(p.specsDirRelative, specId, "resume", await buildDebugRuntimeContext(p)) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1382
|
-
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
|
+
});
|
|
1383
1601
|
return c.json({ runId: handle.runId, sessionId });
|
|
1384
1602
|
});
|
|
1385
1603
|
app.post("/projects/:projectId/specs/:id/explain", async (c) => {
|
|
@@ -1410,7 +1628,10 @@ ${text}
|
|
|
1410
1628
|
"""
|
|
1411
1629
|
`;
|
|
1412
1630
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1413
|
-
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
|
+
});
|
|
1414
1635
|
return c.json({ runId: handle.runId, sessionId });
|
|
1415
1636
|
});
|
|
1416
1637
|
app.delete("/projects/:projectId/specs/:id", async (c) => {
|
|
@@ -2002,7 +2223,9 @@ function createSessionsRoutes(resolveProject) {
|
|
|
2002
2223
|
}
|
|
2003
2224
|
if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
|
|
2004
2225
|
});
|
|
2005
|
-
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
|
+
});
|
|
2006
2229
|
return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
|
|
2007
2230
|
});
|
|
2008
2231
|
app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
|
|
@@ -2082,16 +2305,27 @@ function assertSafeRelativePath(cwd, p) {
|
|
|
2082
2305
|
}
|
|
2083
2306
|
return p;
|
|
2084
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
|
+
}
|
|
2085
2316
|
async function runGit(cwd, args) {
|
|
2317
|
+
const startedAt = Date.now();
|
|
2086
2318
|
try {
|
|
2087
2319
|
const result = await execFileWithoutWindow("git", args, {
|
|
2088
2320
|
cwd,
|
|
2089
2321
|
encoding: "utf8",
|
|
2090
2322
|
maxBuffer: 16 * 1024 * 1024
|
|
2091
2323
|
});
|
|
2324
|
+
recordGitOp(cwd, args, startedAt, true, 0);
|
|
2092
2325
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
2093
2326
|
} catch (err) {
|
|
2094
2327
|
const e = err;
|
|
2328
|
+
recordGitOp(cwd, args, startedAt, false, typeof e.code === "number" ? e.code : void 0);
|
|
2095
2329
|
if (e.code === "ENOENT") {
|
|
2096
2330
|
throw new GitError("git_missing", "git not found on PATH");
|
|
2097
2331
|
}
|
|
@@ -2102,19 +2336,23 @@ async function runGitChecked(cwd, args) {
|
|
|
2102
2336
|
return runGit(cwd, args);
|
|
2103
2337
|
}
|
|
2104
2338
|
async function runGitRaw(cwd, args) {
|
|
2339
|
+
const startedAt = Date.now();
|
|
2105
2340
|
try {
|
|
2106
2341
|
const result = await execFileWithoutWindow("git", args, {
|
|
2107
2342
|
cwd,
|
|
2108
2343
|
encoding: "utf8",
|
|
2109
2344
|
maxBuffer: 16 * 1024 * 1024
|
|
2110
2345
|
});
|
|
2346
|
+
recordGitOp(cwd, args, startedAt, true, 0);
|
|
2111
2347
|
return { stdout: result.stdout, stderr: result.stderr, code: 0 };
|
|
2112
2348
|
} catch (err) {
|
|
2113
2349
|
const e = err;
|
|
2114
2350
|
if (e.code === "ENOENT") {
|
|
2351
|
+
recordGitOp(cwd, args, startedAt, false);
|
|
2115
2352
|
throw new GitError("git_missing", "git not found on PATH");
|
|
2116
2353
|
}
|
|
2117
2354
|
const code = typeof e.code === "number" ? e.code : 1;
|
|
2355
|
+
recordGitOp(cwd, args, startedAt, false, code);
|
|
2118
2356
|
return { stdout: e.stdout ?? "", stderr: e.stderr ?? e.message ?? "", code };
|
|
2119
2357
|
}
|
|
2120
2358
|
}
|
|
@@ -2198,6 +2436,114 @@ async function discard(cwd, opts) {
|
|
|
2198
2436
|
await runGit(cwd, ["clean", "-fd", "--", r.path]);
|
|
2199
2437
|
}
|
|
2200
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
|
+
}
|
|
2201
2547
|
async function stash(cwd, opts) {
|
|
2202
2548
|
if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
|
|
2203
2549
|
throw new GitError("no_paths", "stash requires at least one path");
|
|
@@ -2238,7 +2584,18 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2238
2584
|
const specRel = `${p.specsDirRelative}/${specId}/spec.md`;
|
|
2239
2585
|
const prompt = buildGitOpsPrompt(action, specId, specRel);
|
|
2240
2586
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
2241
|
-
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
|
+
});
|
|
2242
2599
|
return c.json({ runId: handle.runId, sessionId });
|
|
2243
2600
|
});
|
|
2244
2601
|
app.get("/projects/:projectId/specs/:id/debug", async (c) => {
|
|
@@ -2256,21 +2613,7 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2256
2613
|
return c.json({ exists: false, text: "" });
|
|
2257
2614
|
}
|
|
2258
2615
|
});
|
|
2259
|
-
app.
|
|
2260
|
-
const p = await need(c);
|
|
2261
|
-
if (p instanceof Response) return p;
|
|
2262
|
-
const specId = c.req.param("id");
|
|
2263
|
-
const detail = await p.store.read(specId);
|
|
2264
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2265
|
-
try {
|
|
2266
|
-
const changes = await listChanges(p.path);
|
|
2267
|
-
return c.json({ changes });
|
|
2268
|
-
} catch (err) {
|
|
2269
|
-
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2270
|
-
throw err;
|
|
2271
|
-
}
|
|
2272
|
-
});
|
|
2273
|
-
app.post("/projects/:projectId/specs/:id/commit", async (c) => {
|
|
2616
|
+
app.post("/projects/:projectId/specs/:id/stash", async (c) => {
|
|
2274
2617
|
const p = await need(c);
|
|
2275
2618
|
if (p instanceof Response) return p;
|
|
2276
2619
|
const specId = c.req.param("id");
|
|
@@ -2285,30 +2628,122 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2285
2628
|
const obj = body && typeof body === "object" ? body : {};
|
|
2286
2629
|
const message = typeof obj.message === "string" ? obj.message.trim() : "";
|
|
2287
2630
|
const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
|
|
2288
|
-
if (!message) return c.json({ error: "message must not be empty" }, 400);
|
|
2289
2631
|
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2290
2632
|
try {
|
|
2291
|
-
|
|
2292
|
-
return c.json(
|
|
2633
|
+
await stash(p.path, { message: message || `yorz:${specId}`, paths });
|
|
2634
|
+
return c.json({ ok: true });
|
|
2293
2635
|
} catch (err) {
|
|
2294
2636
|
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2295
2637
|
throw err;
|
|
2296
2638
|
}
|
|
2297
2639
|
});
|
|
2298
|
-
app
|
|
2640
|
+
return app;
|
|
2641
|
+
}
|
|
2642
|
+
function buildGitOpsPrompt(action, specId, specRel) {
|
|
2643
|
+
const base = `${skillRef("yorz-git-ops")},然后执行 git 操作:
|
|
2644
|
+
- spec 文档:${specRel}
|
|
2645
|
+
- 请依据 spec 文档与 git status/diff 自行判断本次 spec 关联的变更文件
|
|
2646
|
+
- spec-id:${specId}
|
|
2647
|
+
`;
|
|
2648
|
+
switch (action) {
|
|
2649
|
+
case "commit":
|
|
2650
|
+
return base + `- 动作:git-commit。请基于 spec 文档与 git status/diff,由你自主判断本次 spec 相关的变更文件,执行 \`git add\` + \`git commit\`;commit message 由你生成,不带 scope;禁止 \`git push\` 与 \`git reset --hard\`。
|
|
2651
|
+
`;
|
|
2652
|
+
case "discard":
|
|
2653
|
+
return base + `- 动作:git-discard。请使用 \`git restore --staged --worktree -- <paths>\` 与 \`git clean -fd -- <paths>\` 丢弃 spec 相关的未提交变更;对 untracked 新文件应先在终端输出列表再处理;不要预先 stash 备份;禁止 \`git reset --hard\` 与 \`git push\`。
|
|
2654
|
+
`;
|
|
2655
|
+
case "stash":
|
|
2656
|
+
return base + `- 动作:git-stash。请使用 \`git stash push -m "yorz:${specId}" -- <paths>\` 暂存 spec 相关变更文件;禁止 \`git push\`。
|
|
2657
|
+
`;
|
|
2658
|
+
}
|
|
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) => {
|
|
2299
2679
|
const p = await need(c);
|
|
2300
2680
|
if (p instanceof Response) return p;
|
|
2301
|
-
const specId = c.req.param("id");
|
|
2302
|
-
const detail = await p.store.read(specId);
|
|
2303
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2304
|
-
let body;
|
|
2305
2681
|
try {
|
|
2306
|
-
|
|
2307
|
-
} catch {
|
|
2308
|
-
return c.json({ error:
|
|
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;
|
|
2309
2686
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
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);
|
|
2312
2747
|
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2313
2748
|
try {
|
|
2314
2749
|
await discard(p.path, { paths });
|
|
@@ -2318,25 +2753,23 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2318
2753
|
throw err;
|
|
2319
2754
|
}
|
|
2320
2755
|
});
|
|
2321
|
-
app.post("/projects/:projectId/
|
|
2756
|
+
app.post("/projects/:projectId/git/push", async (c) => {
|
|
2322
2757
|
const p = await need(c);
|
|
2323
2758
|
if (p instanceof Response) return p;
|
|
2324
|
-
const specId = c.req.param("id");
|
|
2325
|
-
const detail = await p.store.read(specId);
|
|
2326
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2327
|
-
let body;
|
|
2328
2759
|
try {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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;
|
|
2332
2765
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const
|
|
2336
|
-
if (
|
|
2766
|
+
});
|
|
2767
|
+
app.post("/projects/:projectId/git/pull", async (c) => {
|
|
2768
|
+
const p = await need(c);
|
|
2769
|
+
if (p instanceof Response) return p;
|
|
2337
2770
|
try {
|
|
2338
|
-
await
|
|
2339
|
-
return c.json({ ok: true });
|
|
2771
|
+
const result = await pull(p.path);
|
|
2772
|
+
return c.json({ ok: true, ...result });
|
|
2340
2773
|
} catch (err) {
|
|
2341
2774
|
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2342
2775
|
throw err;
|
|
@@ -2344,24 +2777,6 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2344
2777
|
});
|
|
2345
2778
|
return app;
|
|
2346
2779
|
}
|
|
2347
|
-
function buildGitOpsPrompt(action, specId, specRel) {
|
|
2348
|
-
const base = `${skillRef("yorz-git-ops")},然后执行 git 操作:
|
|
2349
|
-
- spec 文档:${specRel}
|
|
2350
|
-
- 请依据 spec 文档与 git status/diff 自行判断本次 spec 关联的变更文件
|
|
2351
|
-
- spec-id:${specId}
|
|
2352
|
-
`;
|
|
2353
|
-
switch (action) {
|
|
2354
|
-
case "commit":
|
|
2355
|
-
return base + `- 动作:git-commit。请基于 spec 文档与 git status/diff,由你自主判断本次 spec 相关的变更文件,执行 \`git add\` + \`git commit\`;commit message 由你生成,不带 scope;禁止 \`git push\` 与 \`git reset --hard\`。
|
|
2356
|
-
`;
|
|
2357
|
-
case "discard":
|
|
2358
|
-
return base + `- 动作:git-discard。请使用 \`git restore --staged --worktree -- <paths>\` 与 \`git clean -fd -- <paths>\` 丢弃 spec 相关的未提交变更;对 untracked 新文件应先在终端输出列表再处理;不要预先 stash 备份;禁止 \`git reset --hard\` 与 \`git push\`。
|
|
2359
|
-
`;
|
|
2360
|
-
case "stash":
|
|
2361
|
-
return base + `- 动作:git-stash。请使用 \`git stash push -m "yorz:${specId}" -- <paths>\` 暂存 spec 相关变更文件;禁止 \`git push\`。
|
|
2362
|
-
`;
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
2780
|
const sseLog = () => getLogger().child("sse");
|
|
2366
2781
|
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2367
2782
|
function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
@@ -2553,9 +2968,8 @@ class EventsHub {
|
|
|
2553
2968
|
if (rest === "commands") return this.attachCommandRuns(s, topic, project);
|
|
2554
2969
|
const cm = /^command:(.+)$/.exec(rest);
|
|
2555
2970
|
if (cm) return this.attachCommandRun(s, topic, project, cm[1]);
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
sm = /^spec:([^:]+)$/.exec(rest);
|
|
2971
|
+
if (rest === "changes") return this.attachProjectChanges(s, topic, project);
|
|
2972
|
+
let sm = /^spec:([^:]+)$/.exec(rest);
|
|
2559
2973
|
if (sm) return this.attachSpec(s, topic, project, sm[1]);
|
|
2560
2974
|
sm = /^session:(.+)$/.exec(rest);
|
|
2561
2975
|
if (sm) return this.attachSession(s, topic, project, sm[1]);
|
|
@@ -2593,9 +3007,12 @@ class EventsHub {
|
|
|
2593
3007
|
this.emit(s, topic, "updated", { type: kind, mtime });
|
|
2594
3008
|
});
|
|
2595
3009
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
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) {
|
|
2599
3016
|
this.emit(s, topic, "ready", {});
|
|
2600
3017
|
const { current, unsubscribe } = subscribeGitChanges(project.path, (changes) => {
|
|
2601
3018
|
this.emit(s, topic, "changes-updated", { changes });
|
|
@@ -3986,6 +4403,15 @@ class CommandManager {
|
|
|
3986
4403
|
this.notifyRuns();
|
|
3987
4404
|
this.notifyRun(next);
|
|
3988
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
|
+
});
|
|
3989
4415
|
}
|
|
3990
4416
|
notifyRuns() {
|
|
3991
4417
|
if (this.runsSubscribers.size === 0) return;
|
|
@@ -4808,7 +5234,7 @@ function parseCommitLines(stdout) {
|
|
|
4808
5234
|
function renderConflictSpec(args) {
|
|
4809
5235
|
const { branch, report, now } = args;
|
|
4810
5236
|
const today = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
|
|
4811
|
-
const
|
|
5237
|
+
const nowStamp2 = `${today} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
4812
5238
|
const fileLines = report.files.length ? report.files.map((f) => `- \`${f}\``).join("\n") : "- (无 — 合并失败但未列出冲突文件,请人工核查)";
|
|
4813
5239
|
const commitBlocks = report.files.length ? report.files.map((f) => {
|
|
4814
5240
|
const commits = report.commitsByFile[f] ?? [];
|
|
@@ -4878,7 +5304,7 @@ _暂无_
|
|
|
4878
5304
|
"---",
|
|
4879
5305
|
"stage: plan",
|
|
4880
5306
|
"last_action: 由 worktree 合并冲突自动生成",
|
|
4881
|
-
`updated_at: '${
|
|
5307
|
+
`updated_at: '${nowStamp2}'`,
|
|
4882
5308
|
`summary: 解决 ${branch} 合并到主项目时产生的 ${report.files.length} 个冲突文件`,
|
|
4883
5309
|
"---",
|
|
4884
5310
|
""
|
|
@@ -4934,7 +5360,9 @@ function createApp(opts) {
|
|
|
4934
5360
|
const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
|
|
4935
5361
|
void main.sessions.send(
|
|
4936
5362
|
sessionId,
|
|
4937
|
-
`${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 }
|
|
4938
5366
|
);
|
|
4939
5367
|
}
|
|
4940
5368
|
});
|
|
@@ -4947,6 +5375,7 @@ function createApp(opts) {
|
|
|
4947
5375
|
api.route("/", createSpecsRoutes(resolveProject));
|
|
4948
5376
|
api.route("/", createSessionsRoutes(resolveProject));
|
|
4949
5377
|
api.route("/", createSpecReviewRoutes(resolveProject));
|
|
5378
|
+
api.route("/", createGitRoutes(resolveProject));
|
|
4950
5379
|
api.route("/", createSpecDraftsRoutes(resolveProject));
|
|
4951
5380
|
api.route("/", createWorktreeRoutes(opts.registry, worktreeManager));
|
|
4952
5381
|
api.route("/", createProjectFilesRoutes(resolveProject));
|
|
@@ -5420,6 +5849,7 @@ const watcherLog = () => getLogger().child("watcher");
|
|
|
5420
5849
|
const UNLINK_SETTLE_MS = 80;
|
|
5421
5850
|
class SpecWatcher {
|
|
5422
5851
|
root;
|
|
5852
|
+
cwd;
|
|
5423
5853
|
unlinkSettleMs;
|
|
5424
5854
|
detailListeners = /* @__PURE__ */ new Map();
|
|
5425
5855
|
listListeners = /* @__PURE__ */ new Set();
|
|
@@ -5430,6 +5860,7 @@ class SpecWatcher {
|
|
|
5430
5860
|
watcher = null;
|
|
5431
5861
|
constructor(opts) {
|
|
5432
5862
|
this.root = opts.specsDir ?? join(opts.cwd, ".yorz", "specs");
|
|
5863
|
+
this.cwd = opts.cwd;
|
|
5433
5864
|
this.unlinkSettleMs = opts.unlinkSettleMs ?? UNLINK_SETTLE_MS;
|
|
5434
5865
|
}
|
|
5435
5866
|
async start() {
|
|
@@ -5523,6 +5954,7 @@ class SpecWatcher {
|
|
|
5523
5954
|
this.pendingUnlinks.set(id, timer);
|
|
5524
5955
|
}
|
|
5525
5956
|
emit(id, evt, mtimeMs) {
|
|
5957
|
+
getTelemetry(this.cwd).record("spec.change", { specId: id, kind: evt, mtimeMs });
|
|
5526
5958
|
for (const cb of this.detailListeners.get(id) ?? []) cb(evt, mtimeMs);
|
|
5527
5959
|
for (const cb of this.listListeners) cb();
|
|
5528
5960
|
}
|
|
@@ -5586,6 +6018,29 @@ function usageWindows(usage) {
|
|
|
5586
6018
|
}
|
|
5587
6019
|
return out;
|
|
5588
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
|
+
}
|
|
5589
6044
|
class ClaudeSession {
|
|
5590
6045
|
constructor(id, cwd, isNew) {
|
|
5591
6046
|
this.id = id;
|
|
@@ -5612,6 +6067,7 @@ class ClaudeSession {
|
|
|
5612
6067
|
try {
|
|
5613
6068
|
const q = query({ prompt, options });
|
|
5614
6069
|
let usage;
|
|
6070
|
+
let metrics;
|
|
5615
6071
|
let completed = false;
|
|
5616
6072
|
for await (const msg of q) {
|
|
5617
6073
|
const m = msg;
|
|
@@ -5636,12 +6092,32 @@ class ClaudeSession {
|
|
|
5636
6092
|
}
|
|
5637
6093
|
} else if (m.type === "result") {
|
|
5638
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
|
+
};
|
|
5639
6115
|
} else if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
|
|
5640
6116
|
completed = true;
|
|
5641
|
-
yield { type: "turn-completed", usage };
|
|
6117
|
+
yield { type: "turn-completed", usage, metrics };
|
|
5642
6118
|
}
|
|
5643
6119
|
}
|
|
5644
|
-
if (!completed) yield { type: "turn-completed", usage };
|
|
6120
|
+
if (!completed) yield { type: "turn-completed", usage, metrics };
|
|
5645
6121
|
} catch (err) {
|
|
5646
6122
|
if (ctrl.signal.aborted) return;
|
|
5647
6123
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -5924,7 +6400,11 @@ class CodexSession {
|
|
|
5924
6400
|
yield { type: "error", message: item.message };
|
|
5925
6401
|
}
|
|
5926
6402
|
} else if (ev.type === "turn.completed") {
|
|
5927
|
-
yield {
|
|
6403
|
+
yield {
|
|
6404
|
+
type: "turn-completed",
|
|
6405
|
+
usage: ev.usage,
|
|
6406
|
+
metrics: { usage: normalizeUsage("codex", ev.usage) }
|
|
6407
|
+
};
|
|
5928
6408
|
} else if (ev.type === "turn.failed") {
|
|
5929
6409
|
yield { type: "error", message: ev.error.message };
|
|
5930
6410
|
} else if (ev.type === "error") {
|
|
@@ -6310,7 +6790,14 @@ class OpenCodeSession {
|
|
|
6310
6790
|
for (const part of textParts(data.parts)) {
|
|
6311
6791
|
if (part.type === "text") yield { type: "text", delta: part.text };
|
|
6312
6792
|
}
|
|
6313
|
-
yield {
|
|
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
|
+
};
|
|
6314
6801
|
} catch (err) {
|
|
6315
6802
|
if (this.aborted) return;
|
|
6316
6803
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -6678,9 +7165,11 @@ class SessionManager {
|
|
|
6678
7165
|
* text while sending an expanded prompt to the Agent. Without it, an injected
|
|
6679
7166
|
* hidden prompt would become the title.
|
|
6680
7167
|
*/
|
|
6681
|
-
async send(sid, prompt, titleSource) {
|
|
7168
|
+
async send(sid, prompt, titleSource, meta = {}) {
|
|
6682
7169
|
const runId = randomUUID();
|
|
6683
7170
|
const emitter = this.emitterFor(sid);
|
|
7171
|
+
const telemetry = getTelemetry(this.cwd);
|
|
7172
|
+
const attribution = { specId: meta.specId, trigger: meta.trigger };
|
|
6684
7173
|
let currentSid = sid;
|
|
6685
7174
|
const startedAt = Date.now();
|
|
6686
7175
|
await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
|
|
@@ -6689,18 +7178,57 @@ class SessionManager {
|
|
|
6689
7178
|
});
|
|
6690
7179
|
this.setRunning(sid, true);
|
|
6691
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;
|
|
6692
7190
|
const task = (async () => {
|
|
6693
7191
|
try {
|
|
6694
7192
|
const ls = await this.ensureLive(sid);
|
|
7193
|
+
agentKind = ls.kind;
|
|
6695
7194
|
for await (const ev of ls.session.send(prompt)) {
|
|
6696
7195
|
if (ev.type === "session-started" && ev.sessionId !== currentSid) {
|
|
6697
7196
|
const oldSid = currentSid;
|
|
6698
7197
|
currentSid = ev.sessionId;
|
|
6699
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
|
+
});
|
|
6700
7227
|
}
|
|
6701
7228
|
emitter.emit("event", ev);
|
|
6702
7229
|
}
|
|
6703
7230
|
} catch (err) {
|
|
7231
|
+
dispatchError = err instanceof Error ? err.message : String(err);
|
|
6704
7232
|
agentLog().error("dispatch failed", {
|
|
6705
7233
|
sessionId: currentSid,
|
|
6706
7234
|
runId,
|
|
@@ -6718,6 +7246,17 @@ class SessionManager {
|
|
|
6718
7246
|
runId,
|
|
6719
7247
|
durationMs: Date.now() - startedAt
|
|
6720
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
|
+
});
|
|
6721
7260
|
await this.store.touch(currentSid).catch(() => {
|
|
6722
7261
|
});
|
|
6723
7262
|
this.setRunning(currentSid, false);
|
|
@@ -8909,6 +9448,7 @@ async function lintFile(path, opts = {}) {
|
|
|
8909
9448
|
}
|
|
8910
9449
|
[...new Set([...SPEC_RULES, ...REVIEW_RULES].map((r) => r.id))];
|
|
8911
9450
|
async function runLint(opts) {
|
|
9451
|
+
const startedAt = Date.now();
|
|
8912
9452
|
const cwd = resolve(opts.cwd);
|
|
8913
9453
|
let targets = [];
|
|
8914
9454
|
if (opts.all) {
|
|
@@ -8978,8 +9518,220 @@ async function runLint(opts) {
|
|
|
8978
9518
|
Total: ${errorCount} error(s), ${warnCount} warn(s) across ${reports.length} file(s).
|
|
8979
9519
|
`);
|
|
8980
9520
|
}
|
|
9521
|
+
await recordLintRun(cwd, {
|
|
9522
|
+
fileCount: targets.length,
|
|
9523
|
+
errorCount,
|
|
9524
|
+
warnCount,
|
|
9525
|
+
exitCode,
|
|
9526
|
+
durMs: Date.now() - startedAt
|
|
9527
|
+
});
|
|
8981
9528
|
return { reports, errorCount, warnCount, exitCode };
|
|
8982
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
|
+
}
|
|
8983
9735
|
const program = new Command();
|
|
8984
9736
|
program.name("yorz").description("YorZ CLI — manage the yorz-spec skill.").version(pkg.version);
|
|
8985
9737
|
const uninstallCmd = program.command("uninstall").description("Remove YorZ artifacts (skills, ...).");
|
|
@@ -9043,6 +9795,18 @@ program.command("lint [paths...]").description("Lint spec.md / review.md files f
|
|
|
9043
9795
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9044
9796
|
}
|
|
9045
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
|
+
);
|
|
9046
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(
|
|
9047
9811
|
"--host <address>",
|
|
9048
9812
|
"bind address (default: 127.0.0.1; the service runs shell commands, so exposing it to the network is opt-in)"
|