vinta-ai-workflows 0.1.7
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/CHANGELOG.md +830 -0
- package/LICENSE +21 -0
- package/README.md +547 -0
- package/package.json +27 -0
- package/schemas/README.md +84 -0
- package/schemas/mcp-preflight-cache.v1.schema.json +50 -0
- package/schemas/prs-context-comments.v1.schema.json +51 -0
- package/schemas/prs-context-frontmatter.v1.schema.json +77 -0
- package/schemas/sub-agent.v1.schema.json +105 -0
- package/schemas/vinta-ai-workflows-config.v1.schema.json +283 -0
- package/skills/vinta-analyze-codebase/SKILL.md +325 -0
- package/skills/vinta-bootstrap-ai-tools/SKILL.md +375 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/README.md +50 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/django/notes.md +52 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/medplum/notes.md +45 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/nextjs-app-router/notes.md +35 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/python-package/notes.md +31 -0
- package/skills/vinta-bootstrap-ai-tools/resources/stacks/typescript-monorepo/notes.md +32 -0
- package/skills/vinta-derive-skills/SKILL.md +200 -0
- package/skills/vinta-derive-skills/resources/amend-plan-template.md +355 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/SKILL.md +292 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/resources/one_off_script_base.py +602 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/resources/one_off_script_base.ts +624 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/create-qa-use-cases/SKILL.md +232 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/create-spec/SKILL.md +235 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/SKILL.md +78 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +357 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +418 -0
- package/skills/vinta-derive-skills/resources/implement-plan-template.md +375 -0
- package/skills/vinta-derive-skills/resources/prs-context-template.md +91 -0
- package/skills/vinta-derive-skills/resources/systematic-debugging-mcp-tools.md +125 -0
- package/skills/vinta-derive-skills/resources/systematic-debugging-template.md +167 -0
- package/skills/vinta-derive-subagents/SKILL.md +192 -0
- package/skills/vinta-install-ai-tools-setup/SKILL.md +138 -0
- package/skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs +392 -0
- package/skills/vinta-migrate-plans-specs/SKILL.md +229 -0
- package/skills/vinta-sync-ai-tools/SKILL.md +184 -0
- package/skills/vinta-update-project-skills/SKILL.md +178 -0
- package/skills/vinta-write-agents-md/SKILL.md +192 -0
- package/vinta-ai-workflows.mjs +316 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Setup script: wire ai-tools/{AGENTS.md,skills,agents/*.yaml} into every supported vendor.
|
|
3
|
+
//
|
|
4
|
+
// Source of truth:
|
|
5
|
+
// - ai-tools/AGENTS.md — universal project conventions (markdown).
|
|
6
|
+
// - ai-tools/skills/<name>/ — universal skills (SKILL.md format converges across
|
|
7
|
+
// Claude Code, Cursor, Codex, VS Code Copilot — symlinked).
|
|
8
|
+
// - ai-tools/agents/<name>.yaml — vendor-agnostic sub-agent definitions. This script
|
|
9
|
+
// reads the YAML and emits per-vendor target files in the
|
|
10
|
+
// format each tool expects (markdown, .agent.md, .toml).
|
|
11
|
+
//
|
|
12
|
+
// What this script does:
|
|
13
|
+
// 1. Symlinks ai-tools/AGENTS.md + ai-tools/skills/ into each vendor's expected paths.
|
|
14
|
+
// 2. Reads each ai-tools/agents/<name>.yaml.
|
|
15
|
+
// 3. For every supported vendor, generates a target file with vendor-specific frontmatter
|
|
16
|
+
// / format. Vendor-specific overrides come from the YAML's `overrides:<vendor>:` block;
|
|
17
|
+
// otherwise sensible defaults are derived from the YAML's `access` field
|
|
18
|
+
// (read-only vs read-write).
|
|
19
|
+
//
|
|
20
|
+
// Idempotent: re-run after editing ai-tools/agents/*.yaml and the vendor copies regenerate.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// pnpm setup:ai-tools # all four vendors
|
|
24
|
+
// node ai-tools/scripts/setup-ai-tools.mjs
|
|
25
|
+
// node ai-tools/scripts/setup-ai-tools.mjs --only claude-code,cursor
|
|
26
|
+
// node ai-tools/scripts/setup-ai-tools.mjs --only codex
|
|
27
|
+
//
|
|
28
|
+
// `--only` accepts a comma-separated list of vendor ids or aliases:
|
|
29
|
+
// claude (aliases: claude-code)
|
|
30
|
+
// cursor
|
|
31
|
+
// copilot (aliases: vscode, vscode-copilot, github-copilot)
|
|
32
|
+
// codex (aliases: openai-codex)
|
|
33
|
+
// Vendors not listed are left untouched: their existing generated files / symlinks remain
|
|
34
|
+
// in place; this script does not delete or rewrite them.
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
readdirSync,
|
|
38
|
+
readFileSync,
|
|
39
|
+
writeFileSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
symlinkSync,
|
|
42
|
+
lstatSync,
|
|
43
|
+
rmSync,
|
|
44
|
+
unlinkSync,
|
|
45
|
+
existsSync,
|
|
46
|
+
appendFileSync,
|
|
47
|
+
} from 'node:fs';
|
|
48
|
+
import { join, basename, dirname } from 'node:path';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
import * as YAML from 'yaml';
|
|
51
|
+
|
|
52
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
53
|
+
process.chdir(REPO_ROOT);
|
|
54
|
+
|
|
55
|
+
const AGENTS_SRC = 'ai-tools/agents';
|
|
56
|
+
|
|
57
|
+
// ── CLI: --only <comma-separated vendors> ────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const ALL_VENDORS = ['claude', 'cursor', 'copilot', 'codex'];
|
|
60
|
+
const VENDOR_ALIASES = {
|
|
61
|
+
claude: 'claude',
|
|
62
|
+
'claude-code': 'claude',
|
|
63
|
+
cursor: 'cursor',
|
|
64
|
+
copilot: 'copilot',
|
|
65
|
+
vscode: 'copilot',
|
|
66
|
+
'vscode-copilot': 'copilot',
|
|
67
|
+
'github-copilot': 'copilot',
|
|
68
|
+
codex: 'codex',
|
|
69
|
+
'openai-codex': 'codex',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function parseOnly(argv) {
|
|
73
|
+
const idx = argv.findIndex((a) => a === '--only' || a.startsWith('--only='));
|
|
74
|
+
if (idx === -1) return new Set(ALL_VENDORS);
|
|
75
|
+
const raw = argv[idx].startsWith('--only=')
|
|
76
|
+
? argv[idx].slice('--only='.length)
|
|
77
|
+
: argv[idx + 1];
|
|
78
|
+
if (!raw) {
|
|
79
|
+
console.error('--only requires a comma-separated value, e.g. --only claude-code,cursor');
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
const requested = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
83
|
+
const resolved = new Set();
|
|
84
|
+
for (const id of requested) {
|
|
85
|
+
const canonical = VENDOR_ALIASES[id.toLowerCase()];
|
|
86
|
+
if (!canonical) {
|
|
87
|
+
console.error(
|
|
88
|
+
`unknown vendor "${id}". Allowed: ${Object.keys(VENDOR_ALIASES).sort().join(', ')}`
|
|
89
|
+
);
|
|
90
|
+
process.exit(2);
|
|
91
|
+
}
|
|
92
|
+
resolved.add(canonical);
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SELECTED = parseOnly(process.argv.slice(2));
|
|
98
|
+
|
|
99
|
+
// ── Vendor defaults derived from `access` ────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const DEFAULTS = {
|
|
102
|
+
claude: {
|
|
103
|
+
'read-only': { tools: 'Read, Bash, Glob, Grep' },
|
|
104
|
+
'read-write': { tools: 'Read, Write, Edit, Bash, Glob, Grep, NotebookEdit' },
|
|
105
|
+
},
|
|
106
|
+
cursor: {
|
|
107
|
+
'read-only': { model: 'inherit', readonly: true },
|
|
108
|
+
'read-write': { model: 'inherit', readonly: false },
|
|
109
|
+
},
|
|
110
|
+
copilot: {
|
|
111
|
+
'read-only': { tools: ['codebase', 'search', 'terminal', 'usages'] },
|
|
112
|
+
'read-write': {
|
|
113
|
+
tools: ['codebase', 'editFiles', 'runCommands', 'runTasks', 'search', 'terminal', 'usages'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
codex: {
|
|
117
|
+
'read-only': { sandbox_mode: 'read-only' },
|
|
118
|
+
'read-write': { sandbox_mode: 'workspace-write' },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function ensureSymlink(target, linkPath) {
|
|
125
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
126
|
+
let existing;
|
|
127
|
+
try {
|
|
128
|
+
existing = lstatSync(linkPath);
|
|
129
|
+
} catch {
|
|
130
|
+
existing = null;
|
|
131
|
+
}
|
|
132
|
+
if (existing) {
|
|
133
|
+
if (existing.isSymbolicLink()) {
|
|
134
|
+
unlinkSync(linkPath);
|
|
135
|
+
} else {
|
|
136
|
+
console.warn(`[setup-ai-tools] ${linkPath} exists and is not a symlink — leaving as-is`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
symlinkSync(target, linkPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resetDir(path) {
|
|
144
|
+
rmSync(path, { recursive: true, force: true });
|
|
145
|
+
mkdirSync(path, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveVendorConfig(doc, vendor) {
|
|
149
|
+
// Merge: derived defaults from `access` + everything `doc` declares directly
|
|
150
|
+
// (e.g. `claude-tools` for backwards compat) + per-vendor `overrides`.
|
|
151
|
+
const access = doc.access ?? 'read-write';
|
|
152
|
+
const base = { ...DEFAULTS[vendor]?.[access] };
|
|
153
|
+
// Convenience: top-level `claude-tools` maps to claude.tools
|
|
154
|
+
if (vendor === 'claude' && doc['claude-tools']) {
|
|
155
|
+
base.tools = doc['claude-tools'];
|
|
156
|
+
}
|
|
157
|
+
const overrides = doc.overrides?.[vendor] ?? {};
|
|
158
|
+
return { ...base, ...overrides };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function escapeTomlBasic(s) {
|
|
162
|
+
return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function escapeTomlMultiline(body) {
|
|
166
|
+
// TOML triple-quoted multiline strings: only conflict is the literal `"""`
|
|
167
|
+
// sequence inside the body. Replace with escaped triple-quotes.
|
|
168
|
+
return body.replace(/"""/g, '\\"\\"\\"');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── .gitignore management ────────────────────────────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// `.vinta-ai-workflows/` holds durable working state that is per-developer
|
|
174
|
+
// machine, not committed history:
|
|
175
|
+
// - prs-context/{feature}/{phase}.md — implement-plan / amend-plan PR drafts
|
|
176
|
+
// - cache.yaml — systematic-debugging MCP preflight state
|
|
177
|
+
// Single umbrella entry covers everything. Append on first run if not present.
|
|
178
|
+
|
|
179
|
+
function ensureGitignoreEntries(entries) {
|
|
180
|
+
const path = '.gitignore';
|
|
181
|
+
let current = '';
|
|
182
|
+
try { current = readFileSync(path, 'utf8'); } catch { current = ''; }
|
|
183
|
+
const lines = new Set(current.split('\n').map((l) => l.trim()));
|
|
184
|
+
|
|
185
|
+
const missing = entries.filter((e) => !lines.has(e) && !lines.has(e.replace(/\/$/, '')));
|
|
186
|
+
if (missing.length === 0) return [];
|
|
187
|
+
|
|
188
|
+
const block = (current && !current.endsWith('\n') ? '\n' : '')
|
|
189
|
+
+ (current ? '\n' : '')
|
|
190
|
+
+ '# vinta-ai-workflows / implement-plan working notes\n'
|
|
191
|
+
+ missing.join('\n') + '\n';
|
|
192
|
+
|
|
193
|
+
if (existsSync(path)) appendFileSync(path, block);
|
|
194
|
+
else writeFileSync(path, block.trimStart());
|
|
195
|
+
return missing;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function autoGenHeader(srcPath, comment = '#') {
|
|
199
|
+
return [
|
|
200
|
+
`${comment} AUTO-GENERATED by ai-tools/scripts/setup-ai-tools.mjs from ${srcPath}.`,
|
|
201
|
+
`${comment} Edit ${srcPath}, then run: pnpm setup:ai-tools.`,
|
|
202
|
+
].join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Vendor emitters ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function emitClaude(name, doc, body) {
|
|
208
|
+
const cfg = resolveVendorConfig(doc, 'claude');
|
|
209
|
+
const lines = [
|
|
210
|
+
'---',
|
|
211
|
+
autoGenHeader(`ai-tools/agents/${name}.yaml`),
|
|
212
|
+
`name: ${doc.name}`,
|
|
213
|
+
`description: ${doc.description}`,
|
|
214
|
+
];
|
|
215
|
+
if (cfg.tools) lines.push(`tools: ${cfg.tools}`);
|
|
216
|
+
if (cfg.model) lines.push(`model: ${cfg.model}`);
|
|
217
|
+
lines.push('---', body);
|
|
218
|
+
writeFileSync(`.claude/agents/${name}.md`, lines.join('\n') + '\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function emitCursor(name, doc, body) {
|
|
222
|
+
const cfg = resolveVendorConfig(doc, 'cursor');
|
|
223
|
+
const lines = [
|
|
224
|
+
'---',
|
|
225
|
+
autoGenHeader(`ai-tools/agents/${name}.yaml`),
|
|
226
|
+
`name: ${doc.name}`,
|
|
227
|
+
`description: ${doc.description}`,
|
|
228
|
+
];
|
|
229
|
+
if (cfg.model) lines.push(`model: ${cfg.model}`);
|
|
230
|
+
if (cfg.readonly !== undefined) lines.push(`readonly: ${cfg.readonly}`);
|
|
231
|
+
if (cfg.is_background !== undefined) lines.push(`is_background: ${cfg.is_background}`);
|
|
232
|
+
lines.push('---', body);
|
|
233
|
+
writeFileSync(`.cursor/agents/${name}.md`, lines.join('\n') + '\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function emitCopilot(name, doc, body) {
|
|
237
|
+
const cfg = resolveVendorConfig(doc, 'copilot');
|
|
238
|
+
const lines = [
|
|
239
|
+
'---',
|
|
240
|
+
autoGenHeader(`ai-tools/agents/${name}.yaml`),
|
|
241
|
+
`description: ${doc.description}`,
|
|
242
|
+
];
|
|
243
|
+
if (cfg.tools) lines.push(`tools: ${JSON.stringify(cfg.tools)}`);
|
|
244
|
+
if (cfg.model) lines.push(`model: ${cfg.model}`);
|
|
245
|
+
if (cfg['user-invocable'] !== undefined) lines.push(`user-invocable: ${cfg['user-invocable']}`);
|
|
246
|
+
if (cfg['disable-model-invocation'] !== undefined) {
|
|
247
|
+
lines.push(`disable-model-invocation: ${cfg['disable-model-invocation']}`);
|
|
248
|
+
}
|
|
249
|
+
lines.push('---', body);
|
|
250
|
+
writeFileSync(`.github/agents/${name}.agent.md`, lines.join('\n') + '\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function emitCodex(name, doc, body) {
|
|
254
|
+
const cfg = resolveVendorConfig(doc, 'codex');
|
|
255
|
+
const lines = [
|
|
256
|
+
autoGenHeader(`ai-tools/agents/${name}.yaml`, '#'),
|
|
257
|
+
`name = "${escapeTomlBasic(doc.name)}"`,
|
|
258
|
+
`description = "${escapeTomlBasic(doc.description)}"`,
|
|
259
|
+
];
|
|
260
|
+
if (cfg.sandbox_mode) lines.push(`sandbox_mode = "${cfg.sandbox_mode}"`);
|
|
261
|
+
if (cfg.model) lines.push(`model = "${cfg.model}"`);
|
|
262
|
+
if (cfg.model_reasoning_effort) {
|
|
263
|
+
lines.push(`model_reasoning_effort = "${cfg.model_reasoning_effort}"`);
|
|
264
|
+
}
|
|
265
|
+
lines.push(
|
|
266
|
+
'developer_instructions = """',
|
|
267
|
+
escapeTomlMultiline(body.trimEnd()),
|
|
268
|
+
'"""',
|
|
269
|
+
''
|
|
270
|
+
);
|
|
271
|
+
writeFileSync(`.codex/agents/${name}.toml`, lines.join('\n'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Stage 0: gitignore ───────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
const gitignoreAdded = ensureGitignoreEntries(['.vinta-ai-workflows/']);
|
|
277
|
+
|
|
278
|
+
// ── Stage 1: stable symlinks (skills + AGENTS.md) ────────────────────────
|
|
279
|
+
|
|
280
|
+
// Universal anchors — always set up regardless of --only selection.
|
|
281
|
+
ensureSymlink('ai-tools/AGENTS.md', 'AGENTS.md');
|
|
282
|
+
ensureSymlink('../ai-tools/skills', '.agents/skills');
|
|
283
|
+
|
|
284
|
+
// Vendor-specific symlinks — gated on SELECTED.
|
|
285
|
+
if (SELECTED.has('claude')) {
|
|
286
|
+
ensureSymlink('../ai-tools/skills', '.claude/skills');
|
|
287
|
+
}
|
|
288
|
+
if (SELECTED.has('cursor')) {
|
|
289
|
+
ensureSymlink('../ai-tools/skills', '.cursor/skills');
|
|
290
|
+
}
|
|
291
|
+
if (SELECTED.has('copilot')) {
|
|
292
|
+
ensureSymlink('../ai-tools/AGENTS.md', '.github/copilot-instructions.md');
|
|
293
|
+
ensureSymlink('../ai-tools/skills', '.github/skills');
|
|
294
|
+
}
|
|
295
|
+
// Codex shares `.agents/skills` (already linked above) — no extra symlink needed.
|
|
296
|
+
|
|
297
|
+
// ── Stage 2: regenerate per-vendor sub-agent files ───────────────────────
|
|
298
|
+
|
|
299
|
+
const VENDOR_AGENT_DIRS = {
|
|
300
|
+
claude: '.claude/agents',
|
|
301
|
+
cursor: '.cursor/agents',
|
|
302
|
+
copilot: '.github/agents',
|
|
303
|
+
codex: '.codex/agents',
|
|
304
|
+
};
|
|
305
|
+
const VENDOR_EMITTERS = {
|
|
306
|
+
claude: emitClaude,
|
|
307
|
+
cursor: emitCursor,
|
|
308
|
+
copilot: emitCopilot,
|
|
309
|
+
codex: emitCodex,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Reset only selected vendors' agent dirs; leave the rest untouched.
|
|
313
|
+
for (const vendor of SELECTED) {
|
|
314
|
+
resetDir(VENDOR_AGENT_DIRS[vendor]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const generated = [];
|
|
318
|
+
for (const file of readdirSync(AGENTS_SRC)) {
|
|
319
|
+
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue;
|
|
320
|
+
const name = basename(file, file.endsWith('.yaml') ? '.yaml' : '.yml');
|
|
321
|
+
const src = join(AGENTS_SRC, file);
|
|
322
|
+
const text = readFileSync(src, 'utf8');
|
|
323
|
+
const doc = YAML.parse(text);
|
|
324
|
+
|
|
325
|
+
// Schema: schemas/sub-agent.v1.schema.json (in vinta-ai-workflows package).
|
|
326
|
+
if (doc?.schema_version !== 1) {
|
|
327
|
+
throw new Error(`${src}: missing or unsupported 'schema_version' (expected: 1; see schemas/sub-agent.v1.schema.json)`);
|
|
328
|
+
}
|
|
329
|
+
if (!doc?.name) throw new Error(`${src}: missing required field 'name'`);
|
|
330
|
+
if (!doc?.description) throw new Error(`${src}: missing required field 'description'`);
|
|
331
|
+
if (!doc?.access) throw new Error(`${src}: missing required field 'access' (read-only | read-write)`);
|
|
332
|
+
if (typeof doc.body !== 'string' || !doc.body.trim()) {
|
|
333
|
+
throw new Error(`${src}: missing required field 'body' (markdown content as YAML literal block)`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const vendor of SELECTED) {
|
|
337
|
+
VENDOR_EMITTERS[vendor](name, doc, doc.body);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
generated.push({ name, access: doc.access });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Report ───────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
const selectedList = [...SELECTED];
|
|
346
|
+
const skipped = ALL_VENDORS.filter((v) => !SELECTED.has(v));
|
|
347
|
+
|
|
348
|
+
console.log(`Selected vendors: ${selectedList.join(', ')}`);
|
|
349
|
+
if (skipped.length) {
|
|
350
|
+
console.log(`Skipped (left untouched): ${skipped.join(', ')}`);
|
|
351
|
+
}
|
|
352
|
+
console.log('');
|
|
353
|
+
|
|
354
|
+
if (gitignoreAdded.length) {
|
|
355
|
+
console.log(`.gitignore: appended ${gitignoreAdded.join(', ')}`);
|
|
356
|
+
console.log('');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log('Symlinks ensured:');
|
|
360
|
+
console.log(' AGENTS.md → ai-tools/AGENTS.md');
|
|
361
|
+
console.log(' .agents/skills → ../ai-tools/skills');
|
|
362
|
+
if (SELECTED.has('claude')) {
|
|
363
|
+
console.log(' .claude/skills → ../ai-tools/skills');
|
|
364
|
+
}
|
|
365
|
+
if (SELECTED.has('cursor')) {
|
|
366
|
+
console.log(' .cursor/skills → ../ai-tools/skills');
|
|
367
|
+
}
|
|
368
|
+
if (SELECTED.has('copilot')) {
|
|
369
|
+
console.log(' .github/skills → ../ai-tools/skills');
|
|
370
|
+
console.log(' .github/copilot-instructions.md → ../ai-tools/AGENTS.md');
|
|
371
|
+
}
|
|
372
|
+
console.log('');
|
|
373
|
+
|
|
374
|
+
console.log(
|
|
375
|
+
`Generated ${generated.length * selectedList.length} sub-agent file(s) from ai-tools/agents/*.yaml across ${selectedList.length} vendor(s):`
|
|
376
|
+
);
|
|
377
|
+
const VENDOR_TARGET_DESC = {
|
|
378
|
+
claude: '.claude/agents/<name>.md (markdown)',
|
|
379
|
+
cursor: '.cursor/agents/<name>.md (markdown)',
|
|
380
|
+
copilot: '.github/agents/<name>.agent.md (markdown)',
|
|
381
|
+
codex: '.codex/agents/<name>.toml (TOML)',
|
|
382
|
+
};
|
|
383
|
+
for (const vendor of selectedList) {
|
|
384
|
+
console.log(` ${vendor.padEnd(8)} → ${VENDOR_TARGET_DESC[vendor]}`);
|
|
385
|
+
}
|
|
386
|
+
console.log('');
|
|
387
|
+
for (const { name, access } of generated) {
|
|
388
|
+
console.log(` ${name.padEnd(20)} ${access}`);
|
|
389
|
+
}
|
|
390
|
+
console.log('');
|
|
391
|
+
console.log('Re-run `pnpm setup:ai-tools` after editing ai-tools/agents/*.yaml.');
|
|
392
|
+
console.log('Limit to specific vendors: --only claude-code,cursor,copilot,codex');
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vinta-migrate-plans-specs
|
|
3
|
+
description: Find existing implementation plans and feature specs scattered across a project (`docs/`, `specs/`, `plans/`, root markdown, branch-named files, etc.) and migrate them to the canonical layout `ai-plans/YYYY-MM-DD-{FEATURE_NAME}_{PLAN|SPEC}.md`. Reads each candidate, proposes a target path (date + feature name + classification PLAN vs SPEC), shows the rename diff, and only renames after the user explicitly accepts each one. Run as step 6 of [vinta-bootstrap-ai-tools](../vinta-bootstrap-ai-tools/SKILL.md), or standalone any time afterwards. Read-only by default — every move is gated on per-file approval.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrate plans + specs
|
|
7
|
+
|
|
8
|
+
The Vinta convention: every implementation plan and every feature spec lives under `ai-plans/` at the repo root, named:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
ai-plans/YYYY-MM-DD-{FEATURE_NAME}_{PLAN|SPEC}.md
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `YYYY-MM-DD` — date the doc was authored (use git log first-commit date when migrating, fall back to file mtime, fall back to ask).
|
|
15
|
+
- `FEATURE_NAME` — `UPPERCASE_WITH_UNDERSCORES`. Same prefix shared by a spec and its plan so `ls ai-plans/` groups pairs.
|
|
16
|
+
- `_PLAN` or `_SPEC` — classification. Spec describes *what + why*; plan describes *how + when*.
|
|
17
|
+
|
|
18
|
+
Real-world repos accumulate these docs in many other shapes:
|
|
19
|
+
|
|
20
|
+
| Shape | Examples |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Per-feature folders | `docs/features/<name>/spec.md`, `docs/features/<name>/plan.md` |
|
|
23
|
+
| Flat docs/ | `docs/SPEC-checkout.md`, `docs/PLAN-2025-checkout.md` |
|
|
24
|
+
| Project root | `IMPLEMENTATION_PLAN.md`, `SPEC.md`, `ROADMAP.md` |
|
|
25
|
+
| Branch-named | `feature/checkout/spec.md`, `feat-checkout-plan.md` |
|
|
26
|
+
| Date-prefixed but off-format | `2025-11-checkout-plan.md`, `2025_11_checkout_spec.md` |
|
|
27
|
+
| ADR-style | `docs/adr/0007-checkout.md` (sometimes spec-shaped, sometimes not) |
|
|
28
|
+
| Already in `ai-plans/` but off-format | `ai-plans/checkout-plan.md` (no date), `ai-plans/2025-11-CHECKOUT_IMPLEMENTATION_PLAN.md` (legacy `_IMPLEMENTATION_PLAN`) |
|
|
29
|
+
|
|
30
|
+
This skill normalizes them. Read-only until the user accepts a move.
|
|
31
|
+
|
|
32
|
+
## Inputs
|
|
33
|
+
|
|
34
|
+
- Path to the project being migrated (default: cwd).
|
|
35
|
+
- Optional: list of additional dirs/glob patterns to scan (the user may keep specs somewhere unconventional).
|
|
36
|
+
|
|
37
|
+
## Output
|
|
38
|
+
|
|
39
|
+
- A migration proposal listing each candidate doc with: current path, classification (`PLAN` / `SPEC` / `unclear — ask`), proposed target path, source of date.
|
|
40
|
+
- For each candidate the user accepts: `git mv <old> ai-plans/<new>`. Inbound references inside the repo's markdown updated where possible (best-effort grep + rewrite, with diff shown).
|
|
41
|
+
- A summary report at the end: moved, skipped, ambiguous (user deferred), and a list of follow-ups.
|
|
42
|
+
|
|
43
|
+
## Steps
|
|
44
|
+
|
|
45
|
+
### 1. Prerequisite check
|
|
46
|
+
|
|
47
|
+
`<project>` is a git repo. If not → fall back to plain `mv`, but warn the user that history won't follow the rename. `AskUserQuestion`: `Continue without git mv`, `Stop and init git first`.
|
|
48
|
+
|
|
49
|
+
If `ai-plans/` already exists with files, that's fine — the skill normalizes those too if they're off-format.
|
|
50
|
+
|
|
51
|
+
### 2. Discover candidates
|
|
52
|
+
|
|
53
|
+
Search the repo for plan/spec-shaped markdown. Prioritize signal over recall — false positives waste user time.
|
|
54
|
+
|
|
55
|
+
**Strong signals** (always include):
|
|
56
|
+
|
|
57
|
+
- Filename matches `*spec*.md`, `*plan*.md`, `*roadmap*.md` (case-insensitive).
|
|
58
|
+
- Filename matches `IMPLEMENTATION_PLAN.md`, `SPEC.md`, `RFC.md`, `DESIGN_DOC.md`.
|
|
59
|
+
- Path contains `/specs/`, `/plans/`, `/ai-plans/`, `/rfcs/`, `/design-docs/`.
|
|
60
|
+
- File header (first H1) contains "Spec", "Specification", "Implementation Plan", "RFC", "Design Doc", "Roadmap".
|
|
61
|
+
|
|
62
|
+
**Weak signals** (consider but interview):
|
|
63
|
+
|
|
64
|
+
- Files under `docs/features/<name>/` or `docs/proposals/`.
|
|
65
|
+
- ADR files (`docs/adr/NNNN-*.md`) — usually decision records, not specs, but sometimes a team mixes them.
|
|
66
|
+
|
|
67
|
+
**Always exclude**:
|
|
68
|
+
|
|
69
|
+
- `node_modules/`, `vendor/`, `dist/`, `build/`, `.git/`, `.next/`, build outputs.
|
|
70
|
+
- Generated docs (`docs/api/`, `docs/generated/`).
|
|
71
|
+
- Files already at the canonical path *and* canonical format — they're done.
|
|
72
|
+
|
|
73
|
+
Output a candidate list. If empty → report "no plan/spec docs found" and stop. If the user expected something to be there, ask them where to look.
|
|
74
|
+
|
|
75
|
+
### 3. Classify each candidate (PLAN vs SPEC)
|
|
76
|
+
|
|
77
|
+
For each candidate, decide its bucket. Heuristics:
|
|
78
|
+
|
|
79
|
+
| Signal | Likely class |
|
|
80
|
+
|---|---|
|
|
81
|
+
| Filename / heading contains "spec" / "specification" / "RFC" / "design doc" | `SPEC` |
|
|
82
|
+
| Filename / heading contains "plan" / "implementation plan" / "roadmap" / "phases" | `PLAN` |
|
|
83
|
+
| Body has phased structure (Phase 1, Phase 2, sequential todos) | `PLAN` |
|
|
84
|
+
| Body has acceptance criteria / use cases / hypotheses | `SPEC` |
|
|
85
|
+
| Both signals strongly present | `unclear — ask` |
|
|
86
|
+
| Neither | `unclear — ask` |
|
|
87
|
+
|
|
88
|
+
Don't guess on `unclear — ask`. Surface it to the user with a snippet of the file and let them pick.
|
|
89
|
+
|
|
90
|
+
### 4. Derive the date prefix
|
|
91
|
+
|
|
92
|
+
For each candidate, prefer in this order:
|
|
93
|
+
|
|
94
|
+
1. **Date already in the filename** that obviously corresponds to the doc (e.g. `2025-11-15-checkout-plan.md` → `2025-11-15`). Normalize to `YYYY-MM-DD`.
|
|
95
|
+
2. **Date in the doc body** (e.g. `Date: 2025-11-15`, `Authored: ...` block).
|
|
96
|
+
3. **Git log first-commit date for the file** (`git log --diff-filter=A --follow --format=%aI -- <path> | tail -1`).
|
|
97
|
+
4. **File mtime** as a last resort.
|
|
98
|
+
5. **Ask the user** if the result still looks wrong (e.g. mtime is today because the file got touched by a `find -exec` last week).
|
|
99
|
+
|
|
100
|
+
Always show the chosen date + its source in the proposal so the user can override before accepting the move.
|
|
101
|
+
|
|
102
|
+
### 5. Derive the feature name
|
|
103
|
+
|
|
104
|
+
Strip dates, classification words, and extensions from the filename, then convert to `UPPERCASE_WITH_UNDERSCORES`.
|
|
105
|
+
|
|
106
|
+
| Input | After strip | Final |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `2025-11-15-checkout-plan.md` | `checkout` | `CHECKOUT` |
|
|
109
|
+
| `docs/features/checkout-flow/spec.md` | `checkout-flow` (from parent dir) | `CHECKOUT_FLOW` |
|
|
110
|
+
| `IMPLEMENTATION_PLAN.md` | (filename has no feature) → ask user | `<asked>` |
|
|
111
|
+
| `feat-widget-config-spec.md` | `widget-config` | `WIDGET_CONFIG` |
|
|
112
|
+
| `2026-04-23-WIDGETS_IMPLEMENTATION_PLAN.md` | `WIDGETS` | `WIDGETS` |
|
|
113
|
+
|
|
114
|
+
If the source is a folder like `docs/features/checkout/spec.md` and a sibling `plan.md` exists, **lock both files to the same `FEATURE_NAME`** so the renamed pair groups in `ls`.
|
|
115
|
+
|
|
116
|
+
### 6. Build the proposal
|
|
117
|
+
|
|
118
|
+
Produce a per-candidate report. Group by status. Example:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
ai-plans migration proposal — 7 candidates
|
|
122
|
+
==========================================
|
|
123
|
+
|
|
124
|
+
CLEAR (proposed: git mv to ai-plans/)
|
|
125
|
+
docs/features/checkout/spec.md
|
|
126
|
+
→ ai-plans/2025-11-15-CHECKOUT_SPEC.md
|
|
127
|
+
date: 2025-11-15 (from git log first-commit)
|
|
128
|
+
|
|
129
|
+
docs/features/checkout/plan.md
|
|
130
|
+
→ ai-plans/2025-11-15-CHECKOUT_PLAN.md
|
|
131
|
+
date: 2025-11-15 (paired with spec)
|
|
132
|
+
|
|
133
|
+
IMPLEMENTATION_PLAN.md
|
|
134
|
+
→ ai-plans/2026-02-04-WAREHOUSE_AUDIT_PLAN.md
|
|
135
|
+
date: 2026-02-04 (from doc body "Date: 2026-02-04")
|
|
136
|
+
feature name: asked user (filename had none)
|
|
137
|
+
|
|
138
|
+
ALREADY CANONICAL — only filename normalization
|
|
139
|
+
ai-plans/2026-04-23-WIDGETS_IMPLEMENTATION_PLAN.md
|
|
140
|
+
→ ai-plans/2026-04-23-WIDGETS_PLAN.md
|
|
141
|
+
note: legacy `_IMPLEMENTATION_PLAN` suffix → `_PLAN`
|
|
142
|
+
|
|
143
|
+
UNCLEAR — need user input
|
|
144
|
+
docs/adr/0007-checkout.md
|
|
145
|
+
body has hypothesis + acceptance criteria but is in adr/ folder.
|
|
146
|
+
classify as SPEC, PLAN, or skip?
|
|
147
|
+
|
|
148
|
+
UNTOUCHED (no move proposed)
|
|
149
|
+
docs/api/openapi.md (api docs, excluded)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For each "CLEAR" entry, also list any inbound references discovered by `grep -r "<old-path>" .` so the user knows what else will need to be updated when the file moves.
|
|
153
|
+
|
|
154
|
+
### 7. Per-candidate confirmation
|
|
155
|
+
|
|
156
|
+
**Critical: never move silently.**
|
|
157
|
+
|
|
158
|
+
For each `CLEAR` candidate, ask the user explicitly. Use `AskUserQuestion` with options:
|
|
159
|
+
|
|
160
|
+
- `Apply` — `git mv <old> <new>`; rewrite inbound references.
|
|
161
|
+
- `Skip` — leave the file in place.
|
|
162
|
+
- `Edit proposal` — let the user override the proposed date / feature name / classification, then re-ask.
|
|
163
|
+
|
|
164
|
+
For each `UNCLEAR` candidate, the question is upstream:
|
|
165
|
+
|
|
166
|
+
- `Classify as SPEC, then propose target` — flips to `CLEAR` flow.
|
|
167
|
+
- `Classify as PLAN, then propose target` — same.
|
|
168
|
+
- `Skip — neither` — leave alone.
|
|
169
|
+
- `Show full file body` — re-display before deciding.
|
|
170
|
+
|
|
171
|
+
Do **not** batch under a single "apply all?" prompt. Each move is a separate decision so the user can keep edge cases unchanged. After all decisions, optionally offer a `Apply remaining clear ones` sweep — but only after every candidate has been individually classified and previewed.
|
|
172
|
+
|
|
173
|
+
### 8. Apply approved moves
|
|
174
|
+
|
|
175
|
+
For each accepted candidate:
|
|
176
|
+
|
|
177
|
+
1. `mkdir -p ai-plans` (idempotent).
|
|
178
|
+
2. `git mv <old> ai-plans/<new>` (or plain `mv` if not in a git repo).
|
|
179
|
+
3. Search the repo for inbound references (`grep -rln "<old-relative-path>" --exclude-dir={node_modules,.git,dist,build}`). For each hit:
|
|
180
|
+
- Rewrite the path text to the new path.
|
|
181
|
+
- Show the rewrite diff inline; ask once whether to apply (default `yes`, `no`, `edit`).
|
|
182
|
+
4. Log: `[moved] <old> → ai-plans/<new>`.
|
|
183
|
+
|
|
184
|
+
If the source file had a sidecar (e.g. `docs/features/checkout/diagram.png` next to the spec): ask the user whether to move it alongside (sub-folder `ai-plans/2025-11-15-CHECKOUT/`) or leave in place. Default: leave in place — the canonical layout is flat markdown.
|
|
185
|
+
|
|
186
|
+
### 9. Final report
|
|
187
|
+
|
|
188
|
+
Echo back, in this order:
|
|
189
|
+
|
|
190
|
+
1. **Moved** — `<old> → ai-plans/<new>` per file. Note inbound-reference rewrites count.
|
|
191
|
+
2. **Skipped by user** — files left in place (with reason if the user gave one).
|
|
192
|
+
3. **Unclear, deferred** — files the user said `Show body / Skip — neither` on. Surface them as TODO so they don't get lost.
|
|
193
|
+
4. **Already canonical** — listed for transparency.
|
|
194
|
+
|
|
195
|
+
If `git status` shows the rename, suggest the next steps:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
git diff --stat
|
|
199
|
+
git commit -m "Migrate plans/specs to ai-plans/ canonical layout"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
After moves are committed, point the user at the foundation skills (`create-spec`, `plan-feature`) so future docs land in the canonical layout from the start. If the project's foundation skill bodies still reference a non-`ai-plans/` path or the legacy `_IMPLEMENTATION_PLAN` suffix, flag it as a follow-up to align them.
|
|
203
|
+
|
|
204
|
+
## Read-only mode
|
|
205
|
+
|
|
206
|
+
If the user asks for a "preview" / "dry-run" / "what would change" — perform steps 1–6 only. Stop before step 7. Print the proposal and exit. Re-invoke without dry-run when ready.
|
|
207
|
+
|
|
208
|
+
When invoked without explicit dry-run intent, still default to **showing all proposals first, then asking per file**. Never start with destructive actions.
|
|
209
|
+
|
|
210
|
+
## Pitfalls
|
|
211
|
+
|
|
212
|
+
- **Mass-renaming based on filename heuristics alone.** A file called `auth-spec.md` might be auto-generated API spec (OpenAPI export). Read the body header before classifying. Strong signals + body shape together — never one alone.
|
|
213
|
+
- **Wrong date prefix.** mtime is the worst source — anything that touched the file (a sweep, a format-on-save) clobbers it. Prefer git log first-commit date. Always show the source so the user can override.
|
|
214
|
+
- **Breaking inbound references.** Wikis, READMEs, prior PRs, and CHANGELOG entries reference plan/spec paths. Run grep for inbound refs *before* moving, show the user what else will change. Apply rewrites in the same batch as the move.
|
|
215
|
+
- **Splitting a paired spec + plan.** When a folder has both, lock the `FEATURE_NAME` so they share the prefix. Don't let the user accidentally rename `spec.md` → `CHECKOUT_FLOW_SPEC.md` and `plan.md` → `CHECKOUT_PLAN.md` (different names break grouping).
|
|
216
|
+
- **Touching ADRs that are decision records, not specs.** `docs/adr/` is its own genre. Default to skip; ask the user before reclassifying.
|
|
217
|
+
- **Renaming inside a feature branch.** Concurrent branches that haven't rebased will hit messy conflicts on next merge. Surface this risk to the user before applying — they may want to coordinate or do migration on `main` first.
|
|
218
|
+
- **Generated docs.** OpenAPI, Storybook MDX, Typedoc output, etc. — exclude by path, don't classify.
|
|
219
|
+
- **Stale `_IMPLEMENTATION_PLAN` suffix.** Some Vinta projects historically used `_IMPLEMENTATION_PLAN.md`. The new standard is `_PLAN.md`. Migrate during this run; flag the foundation skill bodies (project's `ai-tools/skills/plan-feature/SKILL.md`) for alignment if they still mention the old suffix.
|
|
220
|
+
|
|
221
|
+
## Verification
|
|
222
|
+
|
|
223
|
+
1. **Empty project**: no candidates found; skill exits cleanly with "no plan/spec docs found". `ai-plans/` not created.
|
|
224
|
+
2. **Single clear plan**: one `IMPLEMENTATION_PLAN.md` at root → proposal shows 1 candidate, classification = PLAN, date from git log; user accepts → file moved with `git mv`, inbound refs (if any) rewritten.
|
|
225
|
+
3. **Paired spec + plan in folder**: `docs/features/checkout/{spec,plan}.md` → both proposed under same `FEATURE_NAME=CHECKOUT`, dates synced where one's date is missing.
|
|
226
|
+
4. **Already canonical**: no diffs proposed; report lists them as `Already canonical`.
|
|
227
|
+
5. **Legacy suffix**: `ai-plans/2026-04-23-WIDGETS_IMPLEMENTATION_PLAN.md` → proposed rename to `_PLAN.md`; user accepts → renamed; inbound refs rewritten.
|
|
228
|
+
6. **Unclear file**: ADR-style file flagged `unclear`; user picks `Skip`; file left untouched and surfaced in final report.
|
|
229
|
+
7. **Dry-run**: user asks for preview → proposal printed, no `git mv` calls executed, exit clean.
|