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.
- package/.agents/manifest.json +170 -0
- package/.agents/skills/agent-creator/SKILL.md +28 -0
- package/.agents/skills/ai-integration/SKILL.md +69 -0
- package/.agents/skills/back-end-development/SKILL.md +44 -0
- package/.agents/skills/database-management/SKILL.md +111 -0
- package/.agents/skills/front-end-development/SKILL.md +39 -0
- package/.agents/skills/front-end-development/references/component-architecture.md +213 -0
- package/.agents/skills/front-end-development/references/data-fetching-pattern.md +111 -0
- package/.agents/skills/front-end-development/references/state-management.md +223 -0
- package/.agents/skills/front-end-development/references/styling.md +226 -0
- package/.agents/skills/self-improvement/SKILL.md +32 -0
- package/.agents/skills/self-improvement/templates/handoff-template.md +35 -0
- package/.agents/skills/self-improvement/templates/plan-template.md +55 -0
- package/.agents/skills/visual-testing/SKILL.md +40 -0
- package/.agents/teams/web-product/README.md +65 -0
- package/.agents/teams/web-product/workflows/feature-lifecycle.md +101 -0
- package/.github/agents/agent-creator.agent.md +66 -0
- package/.github/agents/cli-dev.agent.md +57 -0
- package/.github/agents/code-reviewer.agent.md +66 -0
- package/.github/agents/documentation-writer.agent.md +62 -0
- package/.github/agents/planning-agent.agent.md +70 -0
- package/.github/agents/product-team-orchestrator.agent.md +82 -0
- package/.github/agents/requirement-analyst.agent.md +65 -0
- package/.vscode/mcp.json +15 -0
- package/README.md +121 -0
- package/docs/agents/README.md +36 -0
- package/docs/cli.md +65 -0
- package/docs/plan.md +95 -0
- package/package.json +30 -0
- package/src/agents/definitions/agent-creator.yaml +68 -0
- package/src/agents/definitions/backend-dev.yaml +69 -0
- package/src/agents/definitions/cli-dev.yaml +59 -0
- package/src/agents/definitions/code-reviewer.yaml +67 -0
- package/src/agents/definitions/documentation-writer.yaml +74 -0
- package/src/agents/definitions/frontend-dev.yaml +68 -0
- package/src/agents/definitions/planning-agent.yaml +72 -0
- package/src/agents/definitions/product-team-orchestrator.yaml +83 -0
- package/src/agents/definitions/requirement-analyst.yaml +67 -0
- package/src/agents/definitions/tester.yaml +81 -0
- package/src/agents/generate.js +213 -0
- package/src/cli.js +398 -0
- package/src/lib/adapters.js +41 -0
- package/src/lib/fs-utils.js +60 -0
- package/src/lib/hash.js +9 -0
- package/src/lib/manifest.js +50 -0
- package/src/lib/migration.js +60 -0
- package/src/lib/prompts.js +104 -0
- package/src/lib/sync-engine.js +319 -0
- package/src/lib/template-registry.js +107 -0
- package/templates/bootstrap/base/.agents/skills/agent-creator/SKILL.md +28 -0
- package/templates/bootstrap/base/.agents/skills/ai-integration/SKILL.md +69 -0
- package/templates/bootstrap/base/.agents/skills/back-end-development/SKILL.md +44 -0
- package/templates/bootstrap/base/.agents/skills/database-management/SKILL.md +111 -0
- package/templates/bootstrap/base/.agents/skills/front-end-development/SKILL.md +39 -0
- package/templates/bootstrap/base/.agents/skills/front-end-development/references/component-architecture.md +213 -0
- package/templates/bootstrap/base/.agents/skills/front-end-development/references/data-fetching-pattern.md +111 -0
- package/templates/bootstrap/base/.agents/skills/front-end-development/references/state-management.md +223 -0
- package/templates/bootstrap/base/.agents/skills/front-end-development/references/styling.md +226 -0
- package/templates/bootstrap/base/.agents/skills/self-improvement/SKILL.md +32 -0
- package/templates/bootstrap/base/.agents/skills/self-improvement/templates/handoff-template.md +35 -0
- package/templates/bootstrap/base/.agents/skills/self-improvement/templates/plan-template.md +55 -0
- package/templates/bootstrap/base/.agents/skills/visual-testing/SKILL.md +40 -0
- package/templates/bootstrap/base/.agents/teams/web-product/README.md +65 -0
- package/templates/bootstrap/base/.agents/teams/web-product/workflows/feature-lifecycle.md +101 -0
- package/templates/bootstrap/manifest.json +32 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/agent-creator.md +55 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/backend-dev.md +48 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/cli-dev.md +47 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/code-reviewer.md +52 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/documentation-writer.md +56 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/frontend-dev.md +47 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/planning-agent.md +51 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/product-team-orchestrator.md +51 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/requirement-analyst.md +54 -0
- package/templates/bootstrap/overlays/claude-code/.claude/agents/tester.md +58 -0
- package/templates/bootstrap/overlays/claude-code/.mcp.json +3 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/agent-creator.agent.md +66 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/backend-dev.agent.md +67 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/cli-dev.agent.md +57 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/code-reviewer.agent.md +64 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/documentation-writer.agent.md +72 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/frontend-dev.agent.md +66 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/planning-agent.agent.md +70 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/product-team-orchestrator.agent.md +82 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/requirement-analyst.agent.md +65 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/tester-agent.agent.md +69 -0
- package/templates/bootstrap/overlays/vscode-copilot/.github/agents/tester.agent.md +80 -0
- package/templates/bootstrap/overlays/vscode-copilot/.vscode/mcp.json +12 -0
- package/tests/cli.integration.test.js +63 -0
- package/tests/hash.test.js +9 -0
- 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
|