@agentstorm/server 0.2.5

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.
@@ -0,0 +1,572 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import { safeParseWorkflowSpec } from "@agentstorm/protocol";
6
+ import { nowBeijing } from "@agentstorm/kernel";
7
+ import { assertInsidePackage, assertWorkflowDisplayName, recordWorkflowPackageVersion, safePackageRelativePath, workflowPackageExists, workflowPackagePaths, workflowPromptRelativePath, writeJsonAtomic as writePackageJsonAtomic, } from "./workflow-package.js";
8
+ const TEMPLATE_ROOT_NAME = "templates";
9
+ const LEGACY_TEMPLATE_ROOT_NAME = "agentstorm/templates";
10
+ const TEMPLATE_ROOT_MIGRATION_ID = "nested-template-root-v1";
11
+ const MAX_ASSET_BYTES = 8 * 1024 * 1024;
12
+ const TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
13
+ const BUILTIN_TEMPLATE_IDS = ["general-workflow", "sonic-pipeline"];
14
+ const BUILTIN_TEMPLATE_ROOT = resolveBuiltinTemplateRoot();
15
+ const RETIRED_SEED_TEMPLATES = {
16
+ "grpc-migration": "gRPC migration 示例",
17
+ "review-loop": "审核循环",
18
+ };
19
+ /**
20
+ * The checked-in packages are bootstrap seeds. Once copied into
21
+ * AGENTSTORM_HOME, user edits are authoritative and upgrades do not overwrite
22
+ * them.
23
+ */
24
+ const DEFAULT_TEMPLATES = BUILTIN_TEMPLATE_IDS.map(loadSeedTemplate);
25
+ export function resolveBuiltinTemplateRoot(options) {
26
+ const configured = options?.home?.trim() || process.env.AGENTSTORM_BUILTIN_TEMPLATE_HOME?.trim();
27
+ if (configured)
28
+ return path.resolve(expandHome(configured));
29
+ const electronResourcesPath = options?.resourcesPath?.trim() ||
30
+ process.resourcesPath?.trim();
31
+ if (electronResourcesPath) {
32
+ return path.resolve(electronResourcesPath, TEMPLATE_ROOT_NAME);
33
+ }
34
+ const moduleDirectory = path.dirname(fileURLToPath(options?.moduleUrl ?? import.meta.url));
35
+ const packageTemplates = path.resolve(moduleDirectory, "templates");
36
+ if (fs.existsSync(packageTemplates))
37
+ return packageTemplates;
38
+ return path.resolve(moduleDirectory, "../../../templates");
39
+ }
40
+ function loadSeedTemplate(id) {
41
+ const dir = path.join(BUILTIN_TEMPLATE_ROOT, id);
42
+ const manifest = readJson(path.join(dir, "manifest.json"));
43
+ if (manifest.schemaVersion !== 1 || manifest.id !== id) {
44
+ throw new Error(`内置模板 ${id} 的 manifest 无效`);
45
+ }
46
+ const spec = readJson(path.join(dir, "workflow.json"));
47
+ const layout = readJson(path.join(dir, "layout.json"));
48
+ const parsed = safeParseWorkflowSpec(spec);
49
+ if (!parsed.success)
50
+ throw new Error(`内置模板 ${id} 的 WorkflowSpec 无效`);
51
+ const assets = {};
52
+ for (const config of Object.values(parsed.data.agents)) {
53
+ if (!config.prompt?.startsWith("@"))
54
+ continue;
55
+ const relative = safeRelativePath(config.prompt.slice(1));
56
+ if (!relative)
57
+ throw new Error(`内置模板 ${id} 的 Prompt 路径无效`);
58
+ const target = path.resolve(dir, relative);
59
+ assertInside(dir, target);
60
+ assets[relative] = fs.readFileSync(target, "utf8");
61
+ }
62
+ return {
63
+ id,
64
+ name: manifest.name,
65
+ description: manifest.description,
66
+ spec,
67
+ layout,
68
+ assets,
69
+ seedVersion: manifest.version,
70
+ seedUpdatedAt: manifest.updatedAt,
71
+ };
72
+ }
73
+ export function resolveTemplateRoot(options) {
74
+ const configured = options?.home?.trim() || process.env.AGENTSTORM_TEMPLATE_HOME?.trim();
75
+ if (configured)
76
+ return path.resolve(expandHome(configured));
77
+ const agentstormHome = process.env.AGENTSTORM_HOME?.trim() || "~/.agentstorm";
78
+ return path.resolve(expandHome(path.join(agentstormHome, TEMPLATE_ROOT_NAME)));
79
+ }
80
+ export class GlobalTemplateLibrary {
81
+ rootPath;
82
+ constructor(options) {
83
+ this.rootPath = resolveTemplateRoot(options);
84
+ if (!options?.home?.trim() && !process.env.AGENTSTORM_TEMPLATE_HOME?.trim()) {
85
+ migrateNestedTemplateRoot(this.rootPath);
86
+ }
87
+ this.migrateGeneralWorkflowId();
88
+ this.retireLegacySeedTemplates();
89
+ this.ensureSeedTemplates();
90
+ }
91
+ list() {
92
+ this.ensureSeedTemplates();
93
+ return fs
94
+ .readdirSync(this.rootPath, { withFileTypes: true })
95
+ .filter((entry) => entry.isDirectory() && isSafeTemplateId(entry.name))
96
+ .flatMap((entry) => {
97
+ try {
98
+ const document = this.get(entry.name);
99
+ return [toSummary(document)];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ })
105
+ .sort((left, right) => left.name.localeCompare(right.name));
106
+ }
107
+ get(id) {
108
+ assertTemplateId(id);
109
+ const dir = this.templateDir(id);
110
+ const manifest = readJson(path.join(dir, "manifest.json"));
111
+ if (manifest.schemaVersion !== 1 ||
112
+ manifest.id !== id ||
113
+ !Number.isSafeInteger(manifest.version)) {
114
+ throw new Error(`模板 ${id} 的 manifest 无效`);
115
+ }
116
+ const parsed = safeParseWorkflowSpec(readJson(path.join(dir, "workflow.json")));
117
+ if (!parsed.success)
118
+ throw new Error(`模板 ${id} 的 WorkflowSpec 无效`);
119
+ const layout = parseLayout(readJson(path.join(dir, "layout.json")));
120
+ const assets = readTemplateAssets(dir, parsed.data);
121
+ return {
122
+ ...manifest,
123
+ spec: parsed.data,
124
+ layout,
125
+ assets,
126
+ etag: templateEtag(manifest, parsed.data, layout, assets),
127
+ };
128
+ }
129
+ create(input) {
130
+ const parsed = parseInput(input);
131
+ const id = input.id?.trim() ? input.id.trim() : slugify(parsed.name);
132
+ assertTemplateId(id);
133
+ if (fs.existsSync(this.templateDir(id)))
134
+ throw new Error(`模板 ${id} 已存在`);
135
+ const now = nowBeijing();
136
+ const manifest = {
137
+ schemaVersion: 1,
138
+ id,
139
+ name: parsed.name,
140
+ description: parsed.description,
141
+ version: 1,
142
+ seeded: false,
143
+ updatedAt: now,
144
+ };
145
+ this.writeDocument(manifest, parsed.spec, parsed.layout, parsed.assets);
146
+ return this.get(id);
147
+ }
148
+ update(id, input, expectedEtag) {
149
+ const current = this.get(id);
150
+ if (expectedEtag && expectedEtag !== current.etag)
151
+ throw new Error("模板 ETag mismatch");
152
+ const parsed = parseInput(input);
153
+ const now = nowBeijing();
154
+ const manifest = {
155
+ ...current,
156
+ name: parsed.name,
157
+ description: input.description === undefined ? current.description : parsed.description,
158
+ version: current.version + 1,
159
+ updatedAt: now,
160
+ };
161
+ this.writeDocument(manifest, parsed.spec, parsed.layout, parsed.assets);
162
+ return this.get(id);
163
+ }
164
+ delete(id) {
165
+ const current = this.get(id);
166
+ fs.rmSync(this.templateDir(id), { recursive: true, force: true });
167
+ const deleted = readJson(path.join(this.rootPath, ".deleted.json"), {});
168
+ deleted[id] = current.updatedAt;
169
+ writeJsonAtomic(path.join(this.rootPath, ".deleted.json"), deleted);
170
+ }
171
+ materialize(id, workspaceRoot, options) {
172
+ const template = this.get(id);
173
+ const spec = applyAgentConfig(template.spec, options?.agentConfig);
174
+ const workflowId = assertWorkflowDisplayName(options?.name?.trim() || spec.name || id);
175
+ if (workflowPackageExists(workspaceRoot, workflowId)) {
176
+ throw new Error(`Workflow “${workflowId}” 已存在,请更换显示名称`);
177
+ }
178
+ const packagePaths = workflowPackagePaths(workspaceRoot, workflowId);
179
+ fs.mkdirSync(packagePaths.root, { recursive: true });
180
+ const raw = JSON.stringify(spec, null, 2);
181
+ for (const [relative, content] of Object.entries(template.assets)) {
182
+ const safe = safePackageRelativePath(relative);
183
+ if (!safe)
184
+ throw new Error(`模板 Prompt 路径无效:${relative}`);
185
+ const target = path.resolve(packagePaths.root, safe);
186
+ assertInsidePackage(packagePaths.root, target);
187
+ if (!fs.existsSync(target)) {
188
+ fs.mkdirSync(path.dirname(target), { recursive: true });
189
+ fs.writeFileSync(target, content, "utf8");
190
+ }
191
+ }
192
+ const now = nowBeijing();
193
+ writePackageJsonAtomic(packagePaths.workflow, spec);
194
+ writePackageJsonAtomic(packagePaths.layout, template.layout);
195
+ writePackageJsonAtomic(packagePaths.manifest, {
196
+ schemaVersion: 1,
197
+ name: workflowId,
198
+ description: template.description,
199
+ version: 1,
200
+ source: { kind: "template", templateId: template.id, templateVersion: template.version },
201
+ createdAt: now,
202
+ updatedAt: now,
203
+ });
204
+ const version = recordWorkflowPackageVersion(packagePaths, raw);
205
+ return {
206
+ id: workflowId,
207
+ spec,
208
+ etag: etagOf(raw),
209
+ updatedAt: nowBeijing(),
210
+ version,
211
+ templateId: template.id,
212
+ templateVersion: template.version,
213
+ };
214
+ }
215
+ ensureSeedTemplates() {
216
+ fs.mkdirSync(this.rootPath, { recursive: true, mode: 0o700 });
217
+ const deleted = readJson(path.join(this.rootPath, ".deleted.json"), {});
218
+ for (const seed of DEFAULT_TEMPLATES) {
219
+ const id = seed.id;
220
+ if (deleted[id])
221
+ continue;
222
+ if (fs.existsSync(this.templateDir(id))) {
223
+ // Earlier development builds stored the complete document inside
224
+ // manifest.json. The current package contract keeps the executable
225
+ // graph, layout and Prompt files in their own files. When those files
226
+ // already exist, trim only the obsolete embedded fields and preserve
227
+ // the user's display name, description and version.
228
+ this.normalizeLegacyManifest(id);
229
+ continue;
230
+ }
231
+ const parsed = parseInput(seed);
232
+ const now = nowBeijing();
233
+ const manifest = {
234
+ schemaVersion: 1,
235
+ id,
236
+ name: parsed.name,
237
+ description: parsed.description,
238
+ version: seed.seedVersion,
239
+ seeded: true,
240
+ updatedAt: seed.seedUpdatedAt || now,
241
+ };
242
+ this.writeDocument(manifest, parsed.spec, parsed.layout, parsed.assets);
243
+ }
244
+ }
245
+ migrateGeneralWorkflowId() {
246
+ const legacyId = "grpc-migration";
247
+ const currentId = "general-workflow";
248
+ const legacyDir = this.templateDir(legacyId);
249
+ const currentDir = this.templateDir(currentId);
250
+ if (!fs.existsSync(legacyDir) || fs.existsSync(currentDir))
251
+ return;
252
+ let manifest;
253
+ try {
254
+ manifest = readJson(path.join(legacyDir, "manifest.json"));
255
+ }
256
+ catch {
257
+ return;
258
+ }
259
+ if (manifest.name !== "通用work flow")
260
+ return;
261
+ fs.renameSync(legacyDir, currentDir);
262
+ writeJsonAtomic(path.join(currentDir, "manifest.json"), {
263
+ ...manifest,
264
+ id: currentId,
265
+ });
266
+ }
267
+ retireLegacySeedTemplates() {
268
+ for (const [id, expectedName] of Object.entries(RETIRED_SEED_TEMPLATES)) {
269
+ const dir = this.templateDir(id);
270
+ if (!fs.existsSync(dir))
271
+ continue;
272
+ let manifest;
273
+ try {
274
+ manifest = readJson(path.join(dir, "manifest.json"));
275
+ }
276
+ catch {
277
+ continue;
278
+ }
279
+ if (manifest.seeded && manifest.version === 1 && manifest.name === expectedName) {
280
+ fs.rmSync(dir, { recursive: true, force: true });
281
+ }
282
+ }
283
+ }
284
+ normalizeLegacyManifest(id) {
285
+ const manifestPath = path.join(this.templateDir(id), "manifest.json");
286
+ if (!fs.existsSync(manifestPath) ||
287
+ !fs.existsSync(path.join(this.templateDir(id), "workflow.json")))
288
+ return;
289
+ let current;
290
+ try {
291
+ current = readJson(manifestPath);
292
+ }
293
+ catch {
294
+ return;
295
+ }
296
+ if (!("spec" in current) && !("layout" in current) && !("assets" in current))
297
+ return;
298
+ const now = nowBeijing();
299
+ writeJsonAtomic(manifestPath, {
300
+ schemaVersion: 1,
301
+ id,
302
+ name: typeof current.name === "string" && current.name.trim() ? current.name : id,
303
+ description: typeof current.description === "string" ? current.description : "",
304
+ version: Number.isSafeInteger(current.version) && Number(current.version) > 0
305
+ ? Number(current.version)
306
+ : 1,
307
+ seeded: current.seeded !== false,
308
+ updatedAt: typeof current.updatedAt === "string" ? current.updatedAt : now,
309
+ });
310
+ }
311
+ templateDir(id) {
312
+ return path.join(this.rootPath, id);
313
+ }
314
+ writeDocument(manifest, spec, layout, assets) {
315
+ const dir = this.templateDir(manifest.id);
316
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
317
+ writeJsonAtomic(path.join(dir, "workflow.json"), spec);
318
+ writeJsonAtomic(path.join(dir, "layout.json"), layout);
319
+ // Prompt files are one-to-one with the current Workflow nodes. Remove
320
+ // stale Prompt files left by a node rename/delete before writing the new
321
+ // asset set; otherwise an edited global template would silently retain
322
+ // orphan files that are no longer part of its package.
323
+ fs.rmSync(path.join(dir, "prompts"), { recursive: true, force: true });
324
+ for (const [relative, content] of Object.entries(assets)) {
325
+ const safe = safeRelativePath(relative);
326
+ if (!safe || Buffer.byteLength(content, "utf8") > MAX_ASSET_BYTES) {
327
+ throw new Error(`模板 Prompt 资源无效:${relative}`);
328
+ }
329
+ const target = path.resolve(dir, safe);
330
+ assertInside(dir, target);
331
+ fs.mkdirSync(path.dirname(target), { recursive: true });
332
+ fs.writeFileSync(target, content, "utf8");
333
+ }
334
+ writeJsonAtomic(path.join(dir, "manifest.json"), manifest);
335
+ }
336
+ }
337
+ function parseInput(input) {
338
+ const name = input.name?.trim();
339
+ if (!name)
340
+ throw new Error("模板名称不能为空");
341
+ const parsed = safeParseWorkflowSpec(input.spec);
342
+ if (!parsed.success)
343
+ throw new Error("模板 WorkflowSpec 无效");
344
+ const layout = parseLayout(input.layout ?? {});
345
+ const assets = {};
346
+ for (const [key, value] of Object.entries(input.assets ?? {})) {
347
+ const relative = safeRelativePath(key);
348
+ if (!relative)
349
+ throw new Error(`模板资源路径无效:${key}`);
350
+ if (typeof value !== "string")
351
+ throw new Error(`模板资源必须是 UTF-8 文本:${key}`);
352
+ if (relative in assets)
353
+ throw new Error(`模板资源路径重复:${key}`);
354
+ if (Buffer.byteLength(value, "utf8") > MAX_ASSET_BYTES) {
355
+ throw new Error(`模板资源超过 8 MiB:${key}`);
356
+ }
357
+ assets[relative] = value;
358
+ }
359
+ for (const [agentId, config] of Object.entries(parsed.data.agents)) {
360
+ if (!config.prompt?.startsWith("@"))
361
+ continue;
362
+ const relative = safeRelativePath(config.prompt.slice(1));
363
+ const expected = workflowPromptRelativePath(agentId);
364
+ if (!relative ||
365
+ !relative.startsWith(`prompts${path.sep}`) ||
366
+ relative !== expected ||
367
+ !(relative in assets)) {
368
+ throw new Error(`模板 Prompt 资源缺失:${config.prompt}`);
369
+ }
370
+ }
371
+ return { name, description: input.description?.trim() ?? "", spec: parsed.data, layout, assets };
372
+ }
373
+ function parseLayout(value) {
374
+ if (!value || typeof value !== "object" || Array.isArray(value))
375
+ return {};
376
+ const result = {};
377
+ for (const graph of ["plan", "execute"]) {
378
+ const candidate = value[graph];
379
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
380
+ continue;
381
+ const points = {};
382
+ for (const [id, point] of Object.entries(candidate)) {
383
+ if (!point || typeof point !== "object" || Array.isArray(point))
384
+ continue;
385
+ const x = point.x;
386
+ const y = point.y;
387
+ if (typeof x === "number" &&
388
+ Number.isFinite(x) &&
389
+ typeof y === "number" &&
390
+ Number.isFinite(y)) {
391
+ points[id] = { x, y };
392
+ }
393
+ }
394
+ if (Object.keys(points).length > 0)
395
+ result[graph] = points;
396
+ }
397
+ const rawEdgeHandles = value.edgeHandles;
398
+ if (rawEdgeHandles && typeof rawEdgeHandles === "object" && !Array.isArray(rawEdgeHandles)) {
399
+ const edgeHandles = {};
400
+ for (const graph of ["plan", "execute"]) {
401
+ const candidate = rawEdgeHandles[graph];
402
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
403
+ continue;
404
+ const parsed = {};
405
+ for (const [index, override] of Object.entries(candidate)) {
406
+ if (!/^\d+$/.test(index) || !override || typeof override !== "object")
407
+ continue;
408
+ const sourceHandle = override.sourceHandle;
409
+ const targetHandle = override.targetHandle;
410
+ if (typeof sourceHandle !== "string" && typeof targetHandle !== "string")
411
+ continue;
412
+ parsed[index] = {
413
+ ...(typeof sourceHandle === "string" ? { sourceHandle } : {}),
414
+ ...(typeof targetHandle === "string" ? { targetHandle } : {}),
415
+ };
416
+ }
417
+ if (Object.keys(parsed).length > 0)
418
+ edgeHandles[graph] = parsed;
419
+ }
420
+ if (Object.keys(edgeHandles).length > 0)
421
+ result.edgeHandles = edgeHandles;
422
+ }
423
+ return result;
424
+ }
425
+ function readTemplateAssets(dir, spec) {
426
+ const assets = {};
427
+ for (const [agentId, config] of Object.entries(spec.agents)) {
428
+ if (!config.prompt?.startsWith("@"))
429
+ continue;
430
+ const relative = safeRelativePath(config.prompt.slice(1));
431
+ const expected = workflowPromptRelativePath(agentId);
432
+ if (!relative || !relative.startsWith(`prompts${path.sep}`) || relative !== expected) {
433
+ throw new Error(`模板 Prompt 路径无效:${config.prompt}`);
434
+ }
435
+ if (relative in assets)
436
+ continue;
437
+ const target = path.resolve(dir, relative);
438
+ assertInside(dir, target);
439
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
440
+ throw new Error(`模板 Prompt 不存在:${relative}`);
441
+ }
442
+ assets[relative] = fs.readFileSync(target, "utf8");
443
+ }
444
+ return assets;
445
+ }
446
+ function applyAgentConfig(spec, config) {
447
+ if (!config)
448
+ return spec;
449
+ return {
450
+ ...spec,
451
+ agents: Object.fromEntries(Object.entries(spec.agents).map(([id, current]) => [
452
+ id,
453
+ {
454
+ ...current,
455
+ ...config,
456
+ },
457
+ ])),
458
+ };
459
+ }
460
+ function toSummary(document) {
461
+ const { spec: _spec, layout: _layout, assets: _assets, ...summary } = document;
462
+ return summary;
463
+ }
464
+ function templateEtag(manifest, spec, layout, assets) {
465
+ return etagOf(JSON.stringify({ manifest, spec, layout, assets }));
466
+ }
467
+ function etagOf(raw) {
468
+ return `"${createHash("sha256").update(raw).digest("hex").slice(0, 16)}"`;
469
+ }
470
+ function readJson(filePath, fallback) {
471
+ if (!fs.existsSync(filePath)) {
472
+ if (fallback !== undefined)
473
+ return fallback;
474
+ throw new Error(`文件不存在:${filePath}`);
475
+ }
476
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
477
+ }
478
+ function writeJsonAtomic(filePath, value) {
479
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
480
+ const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
481
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
482
+ fs.renameSync(temporary, filePath);
483
+ }
484
+ /**
485
+ * Early development builds placed templates below
486
+ * `$AGENTSTORM_HOME/agentstorm/templates`. Import that directory once into the
487
+ * final `$AGENTSTORM_HOME/templates` location. Existing destination files win,
488
+ * the source is never modified, and runtime reads only the destination after
489
+ * this function returns.
490
+ */
491
+ function migrateNestedTemplateRoot(targetRoot) {
492
+ const agentstormHome = path.dirname(targetRoot);
493
+ const sourceRoot = path.resolve(agentstormHome, LEGACY_TEMPLATE_ROOT_NAME);
494
+ const markerPath = path.join(agentstormHome, "migrations", `${TEMPLATE_ROOT_MIGRATION_ID}.json`);
495
+ if (sourceRoot === targetRoot || fs.existsSync(markerPath) || !fs.existsSync(sourceRoot))
496
+ return;
497
+ const copied = [];
498
+ const skipped = [];
499
+ copyMissingTemplateEntries(sourceRoot, targetRoot, "", copied, skipped);
500
+ writeJsonAtomic(markerPath, {
501
+ id: TEMPLATE_ROOT_MIGRATION_ID,
502
+ completedAt: nowBeijing(),
503
+ sourceRoot,
504
+ targetRoot,
505
+ copied,
506
+ skipped,
507
+ });
508
+ }
509
+ function copyMissingTemplateEntries(source, target, relative, copied, skipped) {
510
+ const sourceStat = fs.lstatSync(source);
511
+ if (sourceStat.isSymbolicLink()) {
512
+ skipped.push(relative || ".");
513
+ return;
514
+ }
515
+ if (sourceStat.isDirectory()) {
516
+ if (fs.existsSync(target) && !fs.lstatSync(target).isDirectory()) {
517
+ skipped.push(relative || ".");
518
+ return;
519
+ }
520
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
521
+ for (const entry of fs.readdirSync(source)) {
522
+ copyMissingTemplateEntries(path.join(source, entry), path.join(target, entry), relative ? path.join(relative, entry) : entry, copied, skipped);
523
+ }
524
+ return;
525
+ }
526
+ if (!sourceStat.isFile() || fs.existsSync(target)) {
527
+ skipped.push(relative || ".");
528
+ return;
529
+ }
530
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
531
+ fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
532
+ copied.push(relative);
533
+ }
534
+ function expandHome(value) {
535
+ if (value === "~")
536
+ return process.env.HOME ?? value;
537
+ if (value.startsWith("~/"))
538
+ return path.join(process.env.HOME ?? value.slice(2), value.slice(2));
539
+ return value;
540
+ }
541
+ function isSafeTemplateId(value) {
542
+ return TEMPLATE_ID_PATTERN.test(value);
543
+ }
544
+ function assertTemplateId(value) {
545
+ if (!isSafeTemplateId(value))
546
+ throw new Error("模板 ID 只能包含小写字母、数字、点、短横线和下划线");
547
+ }
548
+ function slugify(value) {
549
+ const slug = value
550
+ .toLowerCase()
551
+ .replace(/[^a-z0-9]+/g, "-")
552
+ .replace(/^-+|-+$/g, "");
553
+ return slug || `template-${Date.now()}`;
554
+ }
555
+ function safeRelativePath(value) {
556
+ const normalized = value.replaceAll("\\", "/");
557
+ if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized))
558
+ return null;
559
+ const parts = normalized.split("/");
560
+ if (parts.some((part) => !part || part === "." || part === ".."))
561
+ return null;
562
+ return parts.join(path.sep);
563
+ }
564
+ function assertInside(root, candidate) {
565
+ const resolvedRoot = path.resolve(root);
566
+ const resolvedCandidate = path.resolve(candidate);
567
+ if (resolvedCandidate !== resolvedRoot &&
568
+ !resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`)) {
569
+ throw new Error("路径超出模板根目录");
570
+ }
571
+ }
572
+ //# sourceMappingURL=template-library.js.map
@@ -0,0 +1,88 @@
1
+ {
2
+ "plan": {
3
+ "planner": {
4
+ "x": 163,
5
+ "y": 171
6
+ },
7
+ "plan-reviewer": {
8
+ "x": 389,
9
+ "y": 171
10
+ },
11
+ "approve": {
12
+ "x": 609,
13
+ "y": 169
14
+ },
15
+ "@done": {
16
+ "x": 833,
17
+ "y": 186
18
+ },
19
+ "@blocked": {
20
+ "x": 770,
21
+ "y": 260
22
+ }
23
+ },
24
+ "execute": {
25
+ "worker": {
26
+ "x": 157,
27
+ "y": 180
28
+ },
29
+ "code-reviewer": {
30
+ "x": 390,
31
+ "y": 180
32
+ },
33
+ "tester": {
34
+ "x": 624,
35
+ "y": 179
36
+ },
37
+ "@done": {
38
+ "x": 856,
39
+ "y": 200
40
+ },
41
+ "@blocked": {
42
+ "x": 778,
43
+ "y": 273
44
+ }
45
+ },
46
+ "edgeHandles": {
47
+ "plan": {
48
+ "0": {
49
+ "sourceHandle": "source-right-50",
50
+ "targetHandle": "target-left-60"
51
+ },
52
+ "1": {
53
+ "sourceHandle": "source-right-50",
54
+ "targetHandle": "target-left-60"
55
+ },
56
+ "2": {
57
+ "sourceHandle": "source-bottom-50",
58
+ "targetHandle": "target-bottom-50"
59
+ },
60
+ "3": {
61
+ "sourceHandle": "source-right-50",
62
+ "targetHandle": "target-left-60"
63
+ },
64
+ "4": {
65
+ "sourceHandle": "source-top-40",
66
+ "targetHandle": "target-top-50"
67
+ }
68
+ },
69
+ "execute": {
70
+ "0": {
71
+ "sourceHandle": "source-right-50",
72
+ "targetHandle": "target-left-60"
73
+ },
74
+ "1": {
75
+ "sourceHandle": "source-right-50",
76
+ "targetHandle": "target-left-60"
77
+ },
78
+ "2": {
79
+ "sourceHandle": "source-bottom-50",
80
+ "targetHandle": "target-bottom-50"
81
+ },
82
+ "4": {
83
+ "sourceHandle": "source-top-50",
84
+ "targetHandle": "target-top-50"
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "general-workflow",
4
+ "name": "通用work flow",
5
+ "description": "通用 Plan → 审批 → Execute 流水线。",
6
+ "version": 20,
7
+ "seeded": true,
8
+ "updatedAt": "2026-08-09T16:15:02.000+08:00"
9
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: 通用代码审查 Agent:检查当前任务实现,不代替动态测试
4
+ role: reviewer
5
+ ---
6
+
7
+ # Code Reviewer
8
+
9
+ 你负责审查当前任务的实现质量和静态可验证部分,不修改与审查无关的代码。
10
+
11
+ ## 审查方式
12
+
13
+ 1. 读取任务、验收标准、前置摘要与资源,并检查实际 diff 和相关源码。
14
+ 2. 检查改动是否符合任务边界,是否保持既有 API、错误处理、安全和兼容性约定。
15
+ 3. 查找空实现、遗漏分支、资源泄漏、明显回归、测试缺口和不可维护的复杂度。
16
+ 4. 亲自运行可行的格式检查、类型检查、静态分析或最小导入检查,不只相信自报结果。
17
+
18
+ ## 判定标准
19
+
20
+ 只有关键问题均已解决并有真实证据时才通过。需要返工时指出具体位置、影响和建议修复方向;动态行为留给 Tester 验证,不把“代码看起来正确”当成测试通过。