ai-team 1.0.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 (91) hide show
  1. package/.agents/manifest.json +170 -0
  2. package/.agents/skills/agent-creator/SKILL.md +28 -0
  3. package/.agents/skills/ai-integration/SKILL.md +69 -0
  4. package/.agents/skills/back-end-development/SKILL.md +44 -0
  5. package/.agents/skills/database-management/SKILL.md +111 -0
  6. package/.agents/skills/front-end-development/SKILL.md +39 -0
  7. package/.agents/skills/front-end-development/references/component-architecture.md +213 -0
  8. package/.agents/skills/front-end-development/references/data-fetching-pattern.md +111 -0
  9. package/.agents/skills/front-end-development/references/state-management.md +223 -0
  10. package/.agents/skills/front-end-development/references/styling.md +226 -0
  11. package/.agents/skills/self-improvement/SKILL.md +32 -0
  12. package/.agents/skills/self-improvement/templates/handoff-template.md +35 -0
  13. package/.agents/skills/self-improvement/templates/plan-template.md +55 -0
  14. package/.agents/skills/visual-testing/SKILL.md +40 -0
  15. package/.agents/teams/web-product/README.md +65 -0
  16. package/.agents/teams/web-product/workflows/feature-lifecycle.md +101 -0
  17. package/.github/agents/agent-creator.agent.md +66 -0
  18. package/.github/agents/cli-dev.agent.md +57 -0
  19. package/.github/agents/code-reviewer.agent.md +66 -0
  20. package/.github/agents/documentation-writer.agent.md +62 -0
  21. package/.github/agents/planning-agent.agent.md +70 -0
  22. package/.github/agents/product-team-orchestrator.agent.md +82 -0
  23. package/.github/agents/requirement-analyst.agent.md +65 -0
  24. package/.vscode/mcp.json +15 -0
  25. package/README.md +121 -0
  26. package/docs/agents/README.md +36 -0
  27. package/docs/cli.md +65 -0
  28. package/docs/plan.md +95 -0
  29. package/package.json +30 -0
  30. package/src/agents/definitions/agent-creator.yaml +68 -0
  31. package/src/agents/definitions/backend-dev.yaml +69 -0
  32. package/src/agents/definitions/cli-dev.yaml +59 -0
  33. package/src/agents/definitions/code-reviewer.yaml +67 -0
  34. package/src/agents/definitions/documentation-writer.yaml +74 -0
  35. package/src/agents/definitions/frontend-dev.yaml +68 -0
  36. package/src/agents/definitions/planning-agent.yaml +72 -0
  37. package/src/agents/definitions/product-team-orchestrator.yaml +83 -0
  38. package/src/agents/definitions/requirement-analyst.yaml +67 -0
  39. package/src/agents/definitions/tester.yaml +81 -0
  40. package/src/agents/generate.js +213 -0
  41. package/src/cli.js +398 -0
  42. package/src/lib/adapters.js +41 -0
  43. package/src/lib/fs-utils.js +60 -0
  44. package/src/lib/hash.js +9 -0
  45. package/src/lib/manifest.js +50 -0
  46. package/src/lib/migration.js +60 -0
  47. package/src/lib/prompts.js +104 -0
  48. package/src/lib/sync-engine.js +319 -0
  49. package/src/lib/template-registry.js +107 -0
  50. package/templates/bootstrap/base/.agents/skills/agent-creator/SKILL.md +28 -0
  51. package/templates/bootstrap/base/.agents/skills/ai-integration/SKILL.md +69 -0
  52. package/templates/bootstrap/base/.agents/skills/back-end-development/SKILL.md +44 -0
  53. package/templates/bootstrap/base/.agents/skills/database-management/SKILL.md +111 -0
  54. package/templates/bootstrap/base/.agents/skills/front-end-development/SKILL.md +39 -0
  55. package/templates/bootstrap/base/.agents/skills/front-end-development/references/component-architecture.md +213 -0
  56. package/templates/bootstrap/base/.agents/skills/front-end-development/references/data-fetching-pattern.md +111 -0
  57. package/templates/bootstrap/base/.agents/skills/front-end-development/references/state-management.md +223 -0
  58. package/templates/bootstrap/base/.agents/skills/front-end-development/references/styling.md +226 -0
  59. package/templates/bootstrap/base/.agents/skills/self-improvement/SKILL.md +32 -0
  60. package/templates/bootstrap/base/.agents/skills/self-improvement/templates/handoff-template.md +35 -0
  61. package/templates/bootstrap/base/.agents/skills/self-improvement/templates/plan-template.md +55 -0
  62. package/templates/bootstrap/base/.agents/skills/visual-testing/SKILL.md +40 -0
  63. package/templates/bootstrap/base/.agents/teams/web-product/README.md +65 -0
  64. package/templates/bootstrap/base/.agents/teams/web-product/workflows/feature-lifecycle.md +101 -0
  65. package/templates/bootstrap/manifest.json +32 -0
  66. package/templates/bootstrap/overlays/claude-code/.claude/agents/agent-creator.md +55 -0
  67. package/templates/bootstrap/overlays/claude-code/.claude/agents/backend-dev.md +48 -0
  68. package/templates/bootstrap/overlays/claude-code/.claude/agents/cli-dev.md +47 -0
  69. package/templates/bootstrap/overlays/claude-code/.claude/agents/code-reviewer.md +52 -0
  70. package/templates/bootstrap/overlays/claude-code/.claude/agents/documentation-writer.md +56 -0
  71. package/templates/bootstrap/overlays/claude-code/.claude/agents/frontend-dev.md +47 -0
  72. package/templates/bootstrap/overlays/claude-code/.claude/agents/planning-agent.md +51 -0
  73. package/templates/bootstrap/overlays/claude-code/.claude/agents/product-team-orchestrator.md +51 -0
  74. package/templates/bootstrap/overlays/claude-code/.claude/agents/requirement-analyst.md +54 -0
  75. package/templates/bootstrap/overlays/claude-code/.claude/agents/tester.md +58 -0
  76. package/templates/bootstrap/overlays/claude-code/.mcp.json +3 -0
  77. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/agent-creator.agent.md +66 -0
  78. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/backend-dev.agent.md +67 -0
  79. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/cli-dev.agent.md +57 -0
  80. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/code-reviewer.agent.md +64 -0
  81. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/documentation-writer.agent.md +72 -0
  82. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/frontend-dev.agent.md +66 -0
  83. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/planning-agent.agent.md +70 -0
  84. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/product-team-orchestrator.agent.md +82 -0
  85. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/requirement-analyst.agent.md +65 -0
  86. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/tester-agent.agent.md +69 -0
  87. package/templates/bootstrap/overlays/vscode-copilot/.github/agents/tester.agent.md +80 -0
  88. package/templates/bootstrap/overlays/vscode-copilot/.vscode/mcp.json +12 -0
  89. package/tests/cli.integration.test.js +63 -0
  90. package/tests/hash.test.js +9 -0
  91. package/tests/sync-engine.test.js +77 -0
@@ -0,0 +1,104 @@
1
+ import process from "node:process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { confirm, select, checkbox } from "@inquirer/prompts";
6
+ import YAML from "yaml";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const definitionsDir = path.resolve(__dirname, "../agents/definitions");
10
+
11
+ async function loadAgentDefinitions() {
12
+ const files = await readdir(definitionsDir);
13
+ const yamlFiles = files.filter(
14
+ (f) => f.endsWith(".yaml") || f.endsWith(".yml"),
15
+ );
16
+
17
+ const definitions = [];
18
+ for (const file of yamlFiles) {
19
+ try {
20
+ const raw = await readFile(path.join(definitionsDir, file), "utf8");
21
+ const def = YAML.parse(raw);
22
+ if (def?.name) {
23
+ definitions.push({
24
+ name: def.name,
25
+ emoji: def.emoji ?? "",
26
+ description:
27
+ typeof def.description === "string" ? def.description : "",
28
+ });
29
+ }
30
+ } catch {
31
+ // skip unparseable definitions
32
+ }
33
+ }
34
+
35
+ return definitions.sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+
38
+ export async function collectInteractiveProfile({
39
+ ide,
40
+ team,
41
+ includeMcp,
42
+ agents,
43
+ yes,
44
+ }) {
45
+ if (yes || process.env.CI === "true") {
46
+ return {
47
+ ide: ide ?? "vscode",
48
+ team: team ?? "web-product",
49
+ includeMcp: includeMcp ?? true,
50
+ agents: agents ?? null,
51
+ };
52
+ }
53
+
54
+ const ideAnswer =
55
+ ide ??
56
+ (await select({
57
+ message: "Select IDE",
58
+ default: "vscode",
59
+ choices: [
60
+ { name: "VS Code (GitHub Copilot)", value: "vscode" },
61
+ { name: "Claude Code", value: "claude-code" },
62
+ ],
63
+ }));
64
+
65
+ const teamAnswer =
66
+ team ??
67
+ (await select({
68
+ message: "Select team pack",
69
+ default: "web-product",
70
+ choices: [{ name: "web-product", value: "web-product" }],
71
+ }));
72
+
73
+ let agentsAnswer = agents;
74
+ if (agentsAnswer === undefined || agentsAnswer === null) {
75
+ const definitions = await loadAgentDefinitions();
76
+ const selected = await checkbox({
77
+ message: "Select agents to install",
78
+ instructions: " <space> toggle <a> select all <enter> confirm",
79
+ choices: definitions.map((def) => ({
80
+ name: def.emoji ? `${def.emoji} ${def.name}` : def.name,
81
+ value: def.name,
82
+ checked: true,
83
+ description: def.description,
84
+ })),
85
+ });
86
+ agentsAnswer =
87
+ selected.length > 0 ? selected : definitions.map((d) => d.name);
88
+ }
89
+
90
+ let includeMcpAnswer = includeMcp;
91
+ if (includeMcpAnswer === undefined) {
92
+ includeMcpAnswer = await confirm({
93
+ message: "Include MCP config?",
94
+ default: true,
95
+ });
96
+ }
97
+
98
+ return {
99
+ ide: ideAnswer,
100
+ team: teamAnswer,
101
+ includeMcp: includeMcpAnswer,
102
+ agents: agentsAnswer,
103
+ };
104
+ }
@@ -0,0 +1,319 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { collectTemplateEntries } from "./template-registry.js";
4
+ import {
5
+ loadInstalledManifest,
6
+ writeInstalledManifest,
7
+ createManifest,
8
+ bumpManifest,
9
+ } from "./manifest.js";
10
+ import { readTextIfExists, writeText } from "./fs-utils.js";
11
+ import { hashText } from "./hash.js";
12
+ import { detectLegacyLayout, applyLegacyMigration } from "./migration.js";
13
+
14
+ function classifyConflict(entry, reason, currentHash) {
15
+ return {
16
+ targetPath: entry.targetPath,
17
+ type: "conflict",
18
+ mode: entry.mode,
19
+ reason,
20
+ currentHash,
21
+ sourceHash: entry.sourceHash,
22
+ };
23
+ }
24
+
25
+ function mergeJsonObjects(baseValue, sourceValue, preferSource) {
26
+ if (Array.isArray(baseValue) || Array.isArray(sourceValue)) {
27
+ return preferSource ? sourceValue : baseValue;
28
+ }
29
+
30
+ if (typeof baseValue !== "object" || baseValue === null) {
31
+ return sourceValue;
32
+ }
33
+
34
+ if (typeof sourceValue !== "object" || sourceValue === null) {
35
+ return preferSource ? sourceValue : baseValue;
36
+ }
37
+
38
+ const keys = new Set([
39
+ ...Object.keys(sourceValue),
40
+ ...Object.keys(baseValue),
41
+ ]);
42
+ const result = {};
43
+
44
+ for (const key of keys) {
45
+ if (!(key in baseValue)) {
46
+ result[key] = sourceValue[key];
47
+ continue;
48
+ }
49
+
50
+ if (!(key in sourceValue)) {
51
+ result[key] = baseValue[key];
52
+ continue;
53
+ }
54
+
55
+ result[key] = mergeJsonObjects(
56
+ baseValue[key],
57
+ sourceValue[key],
58
+ preferSource,
59
+ );
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ function mergeMcpJson(existingRaw, sourceRaw, force) {
66
+ const sourceJson = JSON.parse(sourceRaw);
67
+ if (!existingRaw) {
68
+ return `${JSON.stringify(sourceJson, null, 2)}\n`;
69
+ }
70
+
71
+ const existingJson = JSON.parse(existingRaw);
72
+ const merged = {
73
+ ...sourceJson,
74
+ ...existingJson,
75
+ servers: mergeJsonObjects(
76
+ existingJson.servers ?? {},
77
+ sourceJson.servers ?? {},
78
+ force,
79
+ ),
80
+ };
81
+
82
+ return `${JSON.stringify(merged, null, 2)}\n`;
83
+ }
84
+
85
+ function getFileState(manifestFile, currentText) {
86
+ const currentHash = currentText === null ? null : hashText(currentText);
87
+ const installedHash = manifestFile?.installedHash ?? null;
88
+ const localModified = Boolean(
89
+ installedHash && currentHash && currentHash !== installedHash,
90
+ );
91
+
92
+ return {
93
+ currentHash,
94
+ installedHash,
95
+ localModified,
96
+ };
97
+ }
98
+
99
+ export async function planSync({
100
+ targetDir,
101
+ profile,
102
+ force = false,
103
+ command = "plan",
104
+ }) {
105
+ const { templateVersion, schemaVersion, entries } =
106
+ await collectTemplateEntries({
107
+ ide: profile.ide,
108
+ includeMcp: profile.includeMcp,
109
+ agents: profile.agents ?? null,
110
+ });
111
+
112
+ const installedManifest = await loadInstalledManifest(targetDir);
113
+ const actions = [];
114
+ const conflicts = [];
115
+
116
+ for (const entry of entries) {
117
+ const absoluteTargetPath = path.join(targetDir, entry.targetPath);
118
+ const existingText = await readTextIfExists(absoluteTargetPath);
119
+ const manifestFile = installedManifest?.files?.[entry.targetPath] ?? null;
120
+ const state = getFileState(manifestFile, existingText);
121
+
122
+ if (entry.mode === "copy") {
123
+ if (existingText === null) {
124
+ actions.push({ ...entry, action: "create" });
125
+ continue;
126
+ }
127
+
128
+ if (state.currentHash === entry.sourceHash) {
129
+ actions.push({ ...entry, action: "unchanged" });
130
+ continue;
131
+ }
132
+
133
+ if (state.localModified && !force) {
134
+ conflicts.push(
135
+ classifyConflict(entry, "locally-modified", state.currentHash),
136
+ );
137
+ actions.push({ ...entry, action: "skip" });
138
+ continue;
139
+ }
140
+
141
+ actions.push({ ...entry, action: "overwrite" });
142
+ continue;
143
+ }
144
+
145
+ if (entry.mode === "json-merge") {
146
+ try {
147
+ const merged = mergeMcpJson(existingText, entry.sourceContent, force);
148
+ const mergedHash = hashText(merged);
149
+
150
+ if (existingText === merged) {
151
+ actions.push({
152
+ ...entry,
153
+ action: "unchanged",
154
+ mergedContent: merged,
155
+ mergedHash,
156
+ });
157
+ } else {
158
+ actions.push({
159
+ ...entry,
160
+ action: existingText === null ? "create" : "merge",
161
+ mergedContent: merged,
162
+ mergedHash,
163
+ });
164
+ }
165
+ } catch {
166
+ conflicts.push(
167
+ classifyConflict(entry, "invalid-json", state.currentHash),
168
+ );
169
+ actions.push({ ...entry, action: "skip" });
170
+ }
171
+ }
172
+ }
173
+
174
+ const legacyMoves = await detectLegacyLayout(targetDir);
175
+
176
+ return {
177
+ command,
178
+ targetDir,
179
+ profile,
180
+ schemaVersion,
181
+ templateVersion,
182
+ hasManifest: Boolean(installedManifest),
183
+ installedManifest,
184
+ actions,
185
+ conflicts,
186
+ legacyMoves,
187
+ };
188
+ }
189
+
190
+ export async function applySync(plan, { applyMigration = false } = {}) {
191
+ const fileMap = plan.installedManifest?.files
192
+ ? { ...plan.installedManifest.files }
193
+ : {};
194
+ const written = [];
195
+ const skipped = [];
196
+
197
+ for (const action of plan.actions) {
198
+ const absoluteTargetPath = path.join(plan.targetDir, action.targetPath);
199
+
200
+ if (action.action === "skip" || action.action === "unchanged") {
201
+ skipped.push({ targetPath: action.targetPath, reason: action.action });
202
+ continue;
203
+ }
204
+
205
+ const payload =
206
+ action.mode === "json-merge"
207
+ ? action.mergedContent
208
+ : action.sourceContent;
209
+ await writeText(absoluteTargetPath, payload);
210
+
211
+ written.push({ targetPath: action.targetPath, action: action.action });
212
+ fileMap[action.targetPath] = {
213
+ sourcePath: path
214
+ .relative(plan.targetDir, action.sourcePath)
215
+ .split(path.sep)
216
+ .join("/"),
217
+ mode: action.mode,
218
+ sourceHash: action.sourceHash,
219
+ installedHash: hashText(payload),
220
+ updatedAt: new Date().toISOString(),
221
+ };
222
+ }
223
+
224
+ let migration = [];
225
+ if (applyMigration && plan.legacyMoves.length > 0) {
226
+ migration = await applyLegacyMigration(plan.targetDir, plan.legacyMoves);
227
+ }
228
+
229
+ const profile = {
230
+ ide: plan.profile.ide,
231
+ team: plan.profile.team,
232
+ includeMcp: plan.profile.includeMcp,
233
+ agents: plan.profile.agents ?? null,
234
+ };
235
+
236
+ const manifest = plan.hasManifest
237
+ ? bumpManifest(plan.installedManifest, {
238
+ schemaVersion: plan.schemaVersion,
239
+ templateVersion: plan.templateVersion,
240
+ profile,
241
+ files: fileMap,
242
+ })
243
+ : createManifest({
244
+ schemaVersion: plan.schemaVersion,
245
+ templateVersion: plan.templateVersion,
246
+ profile,
247
+ files: fileMap,
248
+ });
249
+
250
+ await writeInstalledManifest(plan.targetDir, manifest);
251
+
252
+ return {
253
+ written,
254
+ skipped,
255
+ conflicts: plan.conflicts,
256
+ migration,
257
+ manifest,
258
+ };
259
+ }
260
+
261
+ export async function doctor({ targetDir }) {
262
+ const manifest = await loadInstalledManifest(targetDir);
263
+ const issues = [];
264
+
265
+ if (!manifest) {
266
+ issues.push({
267
+ level: "error",
268
+ code: "manifest-missing",
269
+ message: "No .agents/manifest.json found",
270
+ });
271
+ return {
272
+ ok: false,
273
+ issues,
274
+ legacyMoves: await detectLegacyLayout(targetDir),
275
+ };
276
+ }
277
+
278
+ const plan = await planSync({
279
+ targetDir,
280
+ profile: manifest.profile,
281
+ force: false,
282
+ command: "doctor",
283
+ });
284
+
285
+ if (manifest.templateVersion !== plan.templateVersion) {
286
+ issues.push({
287
+ level: "warn",
288
+ code: "template-outdated",
289
+ message: `Installed template ${manifest.templateVersion} differs from bundled ${plan.templateVersion}`,
290
+ });
291
+ }
292
+
293
+ for (const [targetPath] of Object.entries(manifest.files ?? {})) {
294
+ const absPath = path.join(targetDir, targetPath);
295
+ try {
296
+ await fs.access(absPath);
297
+ } catch {
298
+ issues.push({
299
+ level: "warn",
300
+ code: "missing-file",
301
+ message: `Missing installed file: ${targetPath}`,
302
+ });
303
+ }
304
+ }
305
+
306
+ for (const conflict of plan.conflicts) {
307
+ issues.push({
308
+ level: "warn",
309
+ code: "local-drift",
310
+ message: `Local drift detected for ${conflict.targetPath}`,
311
+ });
312
+ }
313
+
314
+ return {
315
+ ok: issues.every((issue) => issue.level !== "error"),
316
+ issues,
317
+ legacyMoves: plan.legacyMoves,
318
+ };
319
+ }
@@ -0,0 +1,107 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { promises as fs } from "node:fs";
4
+ import { listFilesRecursive } from "./fs-utils.js";
5
+ import { hashText } from "./hash.js";
6
+ import { getAdapter } from "./adapters.js";
7
+
8
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ export const TEMPLATE_ROOT = path.resolve(dirname, "../../templates/bootstrap");
10
+
11
+ export async function loadTemplateManifest() {
12
+ const filePath = path.join(TEMPLATE_ROOT, "manifest.json");
13
+ const raw = await fs.readFile(filePath, "utf8");
14
+ return JSON.parse(raw);
15
+ }
16
+
17
+ export function getAdapterOverlay(manifest, ide) {
18
+ if (ide === "vscode" || ide === "copilot" || ide === "vscode-copilot") {
19
+ return manifest.packs.overlays.vscode;
20
+ }
21
+
22
+ if (ide === "claude-code" || ide === "claude") {
23
+ return manifest.packs.overlays["claude-code"];
24
+ }
25
+
26
+ throw new Error(`Unsupported IDE: ${ide}`);
27
+ }
28
+
29
+ function agentNameFromFile(filePath) {
30
+ const base = path.basename(filePath);
31
+ // Strip known agent file extensions: .agent.md, .md
32
+ return base.replace(/\.agent\.md$/, "").replace(/\.md$/, "");
33
+ }
34
+
35
+ export async function collectTemplateEntries({ ide, includeMcp, agents }) {
36
+ const manifest = await loadTemplateManifest();
37
+ const adapter = getAdapter(ide);
38
+ const overlay = getAdapterOverlay(manifest, adapter.applyOverlay(ide));
39
+ const baseRoot = path.join(TEMPLATE_ROOT, manifest.baseRoot, ".agents");
40
+ const overlayRoot = path.join(TEMPLATE_ROOT, overlay.root);
41
+
42
+ const baseFiles = await listFilesRecursive(baseRoot);
43
+ const agentsSourceDir = path.join(overlayRoot, overlay.agents);
44
+ const overlayFiles = await listFilesRecursive(agentsSourceDir);
45
+ const entries = [];
46
+
47
+ // Normalise agent filter to a Set for fast lookup (null/undefined = include all)
48
+ const agentFilter =
49
+ Array.isArray(agents) && agents.length > 0 ? new Set(agents) : null;
50
+
51
+ for (const absolutePath of baseFiles) {
52
+ const relative = path.relative(baseRoot, absolutePath);
53
+ const target = adapter.pathMap(
54
+ path.join(".agents", relative).split(path.sep).join("/"),
55
+ );
56
+ const content = await fs.readFile(absolutePath, "utf8");
57
+ entries.push({
58
+ sourcePath: absolutePath,
59
+ targetPath: target,
60
+ mode: "copy",
61
+ sourceHash: hashText(content),
62
+ sourceContent: content,
63
+ });
64
+ }
65
+
66
+ for (const absolutePath of overlayFiles) {
67
+ if (agentFilter !== null) {
68
+ const agentName = agentNameFromFile(absolutePath);
69
+ if (!agentFilter.has(agentName)) {
70
+ continue;
71
+ }
72
+ }
73
+
74
+ const relative = path.relative(agentsSourceDir, absolutePath);
75
+ const target = path
76
+ .join(overlay.agents, relative)
77
+ .split(path.sep)
78
+ .join("/");
79
+ const mappedTarget = adapter.pathMap(target);
80
+ const content = await fs.readFile(absolutePath, "utf8");
81
+ entries.push({
82
+ sourcePath: absolutePath,
83
+ targetPath: mappedTarget,
84
+ mode: "copy",
85
+ sourceHash: hashText(content),
86
+ sourceContent: content,
87
+ });
88
+ }
89
+
90
+ if (includeMcp) {
91
+ const mcpPath = path.join(overlayRoot, overlay.mcp);
92
+ const content = await fs.readFile(mcpPath, "utf8");
93
+ entries.push({
94
+ sourcePath: mcpPath,
95
+ targetPath: adapter.pathMap(overlay.mcp),
96
+ mode: "json-merge",
97
+ sourceHash: hashText(content),
98
+ sourceContent: content,
99
+ });
100
+ }
101
+
102
+ return {
103
+ templateVersion: manifest.templateVersion,
104
+ schemaVersion: manifest.schemaVersion,
105
+ entries,
106
+ };
107
+ }
@@ -0,0 +1,28 @@
1
+ # Agent Creator Skill
2
+
3
+ This skill defines the process and best practices for creating new custom agents in this workspace.
4
+
5
+ ## Reference
6
+
7
+ - [Custom Agents in VS Code Copilot](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
8
+
9
+ ## Purpose
10
+
11
+ Standardize how new agents are defined and integrated into the team workflow.
12
+
13
+ ## Process for Creating a New Agent
14
+
15
+ 1. **Identify the Role**: Determine the specific responsibilities, inputs, and outputs for the agent.
16
+ 2. **Create Agent Definition**: Create a new source definition in `src/agents/definitions/<role-name>.yaml`.
17
+ 3. **Follow the Template**: Use existing definition files as a template (e.g., `src/agents/definitions/backend-dev.yaml`).
18
+ - Define `name`, `description`, `argumentHint`, `target`, `tools`, and `handoffs` in YAML.
19
+ - Write clear instructions in the `body` block.
20
+ 4. **Regenerate Outputs**: Regenerate overlay agent files from definitions.
21
+ 5. **Register Skills**: Ensure the agent has access to relevant skills by listing them in the "Required References" section.
22
+ 6. **Update Documentation**: Add the new agent to `.agents/teams/web-product/README.md` and `docs/agents/README.md` if necessary.
23
+
24
+ ## Best Practices
25
+
26
+ - Keep agent scope focused and single-purpose where possible.
27
+ - Define clear handoffs to other agents (e.g., "Handoff to Tester").
28
+ - Ensure the agent updates knowledge artifacts (`knowledge/`, `plans/`, etc.) as part of its workflow.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: ai-integration
3
+ description: Guidelines for using Gemini with OpenAI compatibility, specifically for handling Structured Outputs and Zod schema limitations.
4
+ ---
5
+
6
+ # AI Integration & Gemini Compatibility
7
+
8
+ This project uses Google Gemini models via the OpenAI compatibility layer. While this allows us to use the standard OpenAI SDK, there are specific constraints and workarounds required for **Structured Outputs** (JSON Schema).
9
+
10
+ For the official list of supported JSON Schema features, refer to the [Gemini API Structured Output Documentation](https://ai.google.dev/gemini-api/docs/structured-output#json-schema-support).
11
+
12
+ ## Zod Schema Compatibility
13
+
14
+ When defining Zod schemas for AI structured outputs, follow these guidelines to ensure compatibility with Gemini:
15
+
16
+ ### 1. Avoid `z.tuple()`
17
+
18
+ Gemini's JSON Schema validator has issues with `prefixItems` which `z.tuple()` generates.
19
+ **❌ Avoid:**
20
+
21
+ ```typescript
22
+ reps: z.tuple([z.number(), z.number()])
23
+ ```
24
+
25
+ **✅ Use:**
26
+
27
+ ```typescript
28
+ reps: z.array(z.number()) // You can add .length(2) if needed, though strict length validation might also be tricky
29
+ ```
30
+
31
+ ### 2. Nullable Fields & Unions
32
+
33
+ Zod's `.nullable()` often generates an `anyOf` structure (e.g., `anyOf: [{type: 'string'}, {type: 'null'}]`). Gemini prefers a single schema with a type array (e.g., `type: ['string', 'null']`).
34
+
35
+ We have implemented a `patchSchema` utility in `server/utils/ai.ts` that automatically handles this conversion at runtime. However, keeping schemas simple helps.
36
+
37
+ ### 3. `exclusiveMinimum`
38
+
39
+ Gemini prefers `minimum` (inclusive) over `exclusiveMinimum`. The `patchSchema` utility automatically converts `exclusiveMinimum` to `minimum + 1` for integers.
40
+
41
+ ## Schema Patching (`server/utils/ai.ts`)
42
+
43
+ We use a custom `patchSchema` function that intercepts the JSON Schema generated by `zodResponseFormat` before sending it to the API.
44
+
45
+ **What it does:**
46
+
47
+ 1. **Flattens `anyOf`**: Recursively flattens nested `anyOf` structures (common with complex unions + nullable).
48
+ 2. **Merges Types**: Collects all types from the flattened options into a single `type` array (e.g., `type: ["integer", "array", "null"]`).
49
+ 3. **Merges Properties**: Naively merges properties from all union members.
50
+ 4. **Converts `exclusiveMinimum`**: Transforms to `minimum`.
51
+
52
+ **Important:** When using this patching logic, we explicitly set `strict: false` in the API request, as the patched schema might not strictly adhere to the original Zod definition's structure (though it validates the same data).
53
+
54
+ ## Example Usage
55
+
56
+ ```typescript
57
+ // Define schema
58
+ const MySchema = z.object({
59
+ name: z.string(),
60
+ tags: z.array(z.string()).nullable(), // Will be patched to type: ['array', 'null']
61
+ })
62
+
63
+ // Call AI
64
+ const response = await callAI({
65
+ // ...
66
+ responseSchema: MySchema,
67
+ schemaName: 'MySchema',
68
+ })
69
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: back-end-development
3
+ description: Best practices for Nuxt/Node backend development, API contracts, error handling, and observability.
4
+ ---
5
+
6
+ # Back-End Development Skill
7
+
8
+ ## Scope
9
+
10
+ Use this skill for:
11
+
12
+ - `server/api/` route implementation
13
+ - validation and API contract design
14
+ - business logic isolation in `server/shared/`
15
+ - data access and migration-safe schema changes
16
+
17
+ ## Standards
18
+
19
+ 1. Define strict input/output schemas.
20
+ 2. Return stable error shapes and HTTP semantics.
21
+ 3. Keep handlers thin; move logic to shared services.
22
+ 4. Add structured logs for failures and key state transitions.
23
+ 5. Enforce idempotency for mutating operations when relevant.
24
+ 6. Protect secrets and sensitive payload fields.
25
+ 7. For each endpoint, add unit tests that cover core functionality and boundary conditions.
26
+
27
+ ## Data & Migration Rules
28
+
29
+ - For DB work, follow `.agents/skills/database-management/SKILL.md`.
30
+ - Migrations must be reversible when feasible.
31
+ - Update RLS policies and verify access paths.
32
+
33
+ ## AI Route Rules
34
+
35
+ - For model integration and structured outputs, follow `.agents/skills/ai-integration/SKILL.md`.
36
+ - Validate model output before persistence.
37
+
38
+ ## Definition of Done
39
+
40
+ - Contract documented and validated
41
+ - Endpoint unit tests cover functionality and boundaries
42
+ - Error paths covered
43
+ - Logging and monitoring hooks added
44
+ - Handoff note includes API examples and edge cases