nitm-ai-dev-toolkit 5.1.1 → 5.1.3
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 +42 -0
- package/README.md +91 -58
- package/bin/cli.js +175 -43
- package/bin/relevance.js +306 -0
- package/package.json +1 -1
- package/src/monorepo/.agents/skills/factory/SKILL.md +2 -1
- package/src/monorepo/.agents/skills/using-git-worktrees/SKILL.md +8 -11
- package/src/monorepo/.claude/prompt-snippets/bootstrap-patch.md +30 -14
- package/src/monorepo/.claude/prompt-snippets/bootstrap-upgrade.md +9 -4
- package/src/monorepo/.claude/prompt-snippets/bootstrap.md +50 -25
- package/src/monorepo/.claude/skills/README.md +7 -7
- package/src/monorepo/.cursor/skills/README.md +8 -8
- package/src/monorepo/.github/skills/README.md +4 -4
- package/src/monorepo/.opencode/commands/review-staged.md +4 -0
- package/src/monorepo/factory/README.md +2 -0
- package/src/repo/.agents/skills/factory/SKILL.md +2 -1
- package/src/repo/.agents/skills/using-git-worktrees/SKILL.md +8 -11
- package/src/repo/.claude/prompt-snippets/bootstrap-patch.md +30 -14
- package/src/repo/.claude/prompt-snippets/bootstrap-upgrade.md +9 -6
- package/src/repo/.claude/prompt-snippets/bootstrap.md +52 -29
- package/src/repo/.claude/skills/README.md +7 -7
- package/src/repo/.cursor/skills/README.md +8 -8
- package/src/repo/.github/skills/README.md +4 -4
- package/src/repo/.opencode/commands/review-staged.md +4 -0
- package/src/repo/factory/README.md +2 -0
package/bin/relevance.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Relevance-aware patch and doctor. This module decides; bin/cli.js plans and writes.
|
|
4
|
+
// - .nitm/retired.json: paths the project retired (never re-added) and an optional tracker override
|
|
5
|
+
// - detectProject(): trackers, Playwright, Prisma, UI and stack facts read from the project
|
|
6
|
+
// - relevanceOf(): add | irrelevant | review for one template path
|
|
7
|
+
// - auditWrapper(): whether a harness wrapper is thin (link to its snippet plus key points) or inlines policy
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const RETIRED_FILE = path.join('.nitm', 'retired.json');
|
|
14
|
+
const TRACKERS = ['github', 'gitlab', 'bitbucket', 'jira', 'linear'];
|
|
15
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
|
+
const MAX_KEY_POINTS = 5;
|
|
17
|
+
const MAX_SUMMARY_WORDS = 30;
|
|
18
|
+
|
|
19
|
+
// Tracker CLI skills ship only for the tracker they talk to. gh-cli also covers GitHub pull requests.
|
|
20
|
+
const TRACKER_SKILLS = { 'gh-cli': 'github', 'glab-cli': 'gitlab', 'bkt-cli': 'bitbucket', acli: 'jira', 'linear-cli': 'linear' };
|
|
21
|
+
// Files named after a tool or stack. They apply only when that tool or stack is detected.
|
|
22
|
+
const STEM_TOPICS = {
|
|
23
|
+
'playwright-test': 'playwright',
|
|
24
|
+
'prisma-migrations': 'prisma',
|
|
25
|
+
'component-structure': 'ui',
|
|
26
|
+
'ui-accessibility': 'ui',
|
|
27
|
+
};
|
|
28
|
+
const TOPIC_NOTES = {
|
|
29
|
+
playwright: 'no Playwright setup detected',
|
|
30
|
+
prisma: 'no Prisma setup detected',
|
|
31
|
+
ui: 'no UI framework detected',
|
|
32
|
+
};
|
|
33
|
+
// Empty MCP templates ship no servers, so a missing copy adds nothing.
|
|
34
|
+
const MCP_TEMPLATES = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
35
|
+
|
|
36
|
+
const HOST_TRACKERS = { 'github.com': 'github', 'gitlab.com': 'gitlab', 'bitbucket.org': 'bitbucket' };
|
|
37
|
+
const UI_DEPS = ['react', 'react-dom', 'react-native', 'expo', 'next', 'vue', 'nuxt', 'svelte', '@sveltejs/kit',
|
|
38
|
+
'@angular/core', 'solid-js', 'astro', 'preact'];
|
|
39
|
+
const PLAYWRIGHT_DEPS = ['playwright', '@playwright/test'];
|
|
40
|
+
const PLAYWRIGHT_CONFIGS = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs'];
|
|
41
|
+
const PRISMA_DEPS = ['prisma', '@prisma/client'];
|
|
42
|
+
const PRISMA_FILES = ['schema.prisma', path.join('prisma', 'schema.prisma')];
|
|
43
|
+
// Root manifests that prove a stack exists (for ui decisions) and that may name Playwright.
|
|
44
|
+
const STACK_MANIFESTS = ['package.json', 'pyproject.toml', 'requirements.txt', 'setup.py', 'composer.json',
|
|
45
|
+
'go.mod', 'Cargo.toml', 'Gemfile', 'pom.xml', 'build.gradle', 'build.gradle.kts'];
|
|
46
|
+
|
|
47
|
+
// Harness wrappers and the shared source each one links to. Snippets hold the policy; a skill with the
|
|
48
|
+
// same name (orient-to-recent-work) is the source when no snippet exists.
|
|
49
|
+
const WRAPPER_KINDS = [
|
|
50
|
+
{ dir: '.claude/rules', snippets: 'rules-snippets' },
|
|
51
|
+
{ dir: '.claude/commands', snippets: 'prompt-snippets' },
|
|
52
|
+
{ dir: '.claude/agents', snippets: 'agents-snippets' },
|
|
53
|
+
{ dir: '.cursor/rules', snippets: 'rules-snippets' },
|
|
54
|
+
{ dir: '.cursor/commands', snippets: 'prompt-snippets' },
|
|
55
|
+
{ dir: '.cursor/agents', snippets: 'agents-snippets' },
|
|
56
|
+
{ dir: '.github/instructions', snippets: 'rules-snippets' },
|
|
57
|
+
{ dir: '.github/prompts', snippets: 'prompt-snippets' },
|
|
58
|
+
{ dir: '.github/agents', snippets: 'agents-snippets' },
|
|
59
|
+
{ dir: '.opencode/rules', snippets: 'rules-snippets' },
|
|
60
|
+
{ dir: '.opencode/commands', snippets: 'prompt-snippets' },
|
|
61
|
+
{ dir: '.opencode/agents', snippets: 'agents-snippets' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function readJsonFile(p) {
|
|
65
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readText(p) {
|
|
69
|
+
try { return fs.readFileSync(p, 'utf8'); } catch (_) { return ''; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- retirement list ----
|
|
73
|
+
|
|
74
|
+
// Canonical form: forward slashes, no leading ./, no doubled or inner / segments, no trailing /. Climbing
|
|
75
|
+
// segments and absolute paths are refused on the raw input, before normalising could fold them away.
|
|
76
|
+
function safeRetiredPath(value, where) {
|
|
77
|
+
const raw = typeof value === 'string' ? value.replace(/\\/g, '/') : '';
|
|
78
|
+
const absolute = path.posix.isAbsolute(raw) || path.win32.isAbsolute(raw);
|
|
79
|
+
const climbs = raw.split('/').includes('..');
|
|
80
|
+
const rel = raw ? path.posix.normalize(raw).replace(/^(\.\/)+/, '').replace(/\/+$/, '') : '';
|
|
81
|
+
if (!rel || rel === '.' || absolute || climbs) {
|
|
82
|
+
throw new Error(`${where}.path must be a relative path inside the project (got ${JSON.stringify(value)})`);
|
|
83
|
+
}
|
|
84
|
+
return rel;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeRetired(entry, index) {
|
|
88
|
+
const where = `${RETIRED_FILE} retired[${index}]`;
|
|
89
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
90
|
+
throw new Error(`${where} must be an object with path, reason and date`);
|
|
91
|
+
}
|
|
92
|
+
const rel = safeRetiredPath(entry.path, where);
|
|
93
|
+
if (typeof entry.reason !== 'string' || !entry.reason.trim()) {
|
|
94
|
+
throw new Error(`${where}.reason is required: say why the file was retired`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof entry.date !== 'string' || !DATE_RE.test(entry.date)) {
|
|
97
|
+
throw new Error(`${where}.date is required as YYYY-MM-DD`);
|
|
98
|
+
}
|
|
99
|
+
return { path: rel, reason: entry.reason.trim(), date: entry.date };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Reads .nitm/retired.json. Absent means nothing retired. A file that exists but is invalid throws, so
|
|
103
|
+
// patch never falls back to re-adding files the project retired.
|
|
104
|
+
function readRetired(cwd) {
|
|
105
|
+
const p = path.join(cwd, RETIRED_FILE);
|
|
106
|
+
if (!fs.existsSync(p)) return { trackers: undefined, entries: [] };
|
|
107
|
+
let data;
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
110
|
+
} catch (ex) {
|
|
111
|
+
throw new Error(`${RETIRED_FILE} exists but is not valid JSON (${ex.message}). Fix or remove it; nothing was written.`);
|
|
112
|
+
}
|
|
113
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
114
|
+
throw new Error(`${RETIRED_FILE} must be a JSON object with "retired" and optional "trackers"`);
|
|
115
|
+
}
|
|
116
|
+
let trackers;
|
|
117
|
+
if (data.trackers !== undefined && data.trackers !== null) {
|
|
118
|
+
if (!Array.isArray(data.trackers) || data.trackers.some((t) => !TRACKERS.includes(t))) {
|
|
119
|
+
throw new Error(`${RETIRED_FILE}: "trackers" must be a list drawn from ${TRACKERS.join(', ')} (use [] for none)`);
|
|
120
|
+
}
|
|
121
|
+
trackers = [...new Set(data.trackers)];
|
|
122
|
+
}
|
|
123
|
+
const list = data.retired === undefined ? [] : data.retired;
|
|
124
|
+
if (!Array.isArray(list)) throw new Error(`${RETIRED_FILE}: "retired" must be a list`);
|
|
125
|
+
return { trackers, entries: list.map(normalizeRetired) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The retirement entry covering a template path (exact file, or any file under a retired directory).
|
|
129
|
+
function retiredFor(key, entries) {
|
|
130
|
+
return entries.find((e) => key === e.path || key.startsWith(e.path + '/')) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- project facts ----
|
|
134
|
+
|
|
135
|
+
// package.json at the root and one level down (apps/*, packages/*), so monorepo dependencies count.
|
|
136
|
+
function packageJsons(cwd) {
|
|
137
|
+
const out = [];
|
|
138
|
+
const pkg = readJsonFile(path.join(cwd, 'package.json'));
|
|
139
|
+
if (pkg) out.push(pkg);
|
|
140
|
+
for (const parent of ['apps', 'packages']) {
|
|
141
|
+
let entries = [];
|
|
142
|
+
try {
|
|
143
|
+
entries = fs.readdirSync(path.join(cwd, parent), { withFileTypes: true });
|
|
144
|
+
} catch (_) { continue; }
|
|
145
|
+
for (const e of entries) {
|
|
146
|
+
const child = readJsonFile(path.join(cwd, parent, e.name, 'package.json'));
|
|
147
|
+
if (e.isDirectory() && child) out.push(child);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dependencyNames(cwd) {
|
|
154
|
+
const names = new Set();
|
|
155
|
+
for (const pkg of packageJsons(cwd)) {
|
|
156
|
+
for (const name of [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]) {
|
|
157
|
+
names.add(name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return names;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The tracker implied by the git origin host, or null when there is no origin or the host is unknown.
|
|
164
|
+
function remoteTrackers(cwd) {
|
|
165
|
+
let url;
|
|
166
|
+
try {
|
|
167
|
+
url = execFileSync('git', ['-C', cwd, 'remote', 'get-url', 'origin'],
|
|
168
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).trim();
|
|
169
|
+
} catch (_) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const match = url.match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@/]+@)?([^/:]+)/i);
|
|
173
|
+
const tracker = match && HOST_TRACKERS[match[1].toLowerCase()];
|
|
174
|
+
return tracker ? [tracker] : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Facts the relevance rules read. `trackers` is null when unknown (the rules then ask for a decision),
|
|
178
|
+
// an array otherwise (possibly empty when the project has no tracker).
|
|
179
|
+
function detectProject(cwd, retired) {
|
|
180
|
+
const deps = dependencyNames(cwd);
|
|
181
|
+
const rootManifests = STACK_MANIFESTS.map((f) => readText(path.join(cwd, f))).join('\n').toLowerCase();
|
|
182
|
+
const configured = retired && retired.trackers !== undefined;
|
|
183
|
+
const trackers = configured ? retired.trackers : remoteTrackers(cwd);
|
|
184
|
+
const trackerSource = configured ? `configured in ${RETIRED_FILE}` : (trackers ? 'git origin' : 'not detected');
|
|
185
|
+
return {
|
|
186
|
+
trackers,
|
|
187
|
+
trackerSource,
|
|
188
|
+
playwright: PLAYWRIGHT_DEPS.some((d) => deps.has(d)) || /playwright/.test(rootManifests)
|
|
189
|
+
|| PLAYWRIGHT_CONFIGS.some((f) => fs.existsSync(path.join(cwd, f))),
|
|
190
|
+
prisma: PRISMA_DEPS.some((d) => deps.has(d)) || PRISMA_FILES.some((f) => fs.existsSync(path.join(cwd, f))),
|
|
191
|
+
ui: UI_DEPS.some((d) => deps.has(d)),
|
|
192
|
+
stackKnown: STACK_MANIFESTS.some((f) => fs.existsSync(path.join(cwd, f))),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- relevance ----
|
|
197
|
+
|
|
198
|
+
// What a template path depends on: 'tracker:<name>', 'playwright', 'prisma', 'ui', 'mcp', or null (generic).
|
|
199
|
+
function topicOf(rel) {
|
|
200
|
+
const n = rel.split(path.sep).join('/');
|
|
201
|
+
if (MCP_TEMPLATES.includes(n)) return 'mcp';
|
|
202
|
+
const skill = /^\.agents\/skills\/([^/]+)\//.exec(n);
|
|
203
|
+
if (skill && TRACKER_SKILLS[skill[1]]) return `tracker:${TRACKER_SKILLS[skill[1]]}`;
|
|
204
|
+
return STEM_TOPICS[path.posix.basename(n).split('.')[0]] || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 'add' applies to the project; 'irrelevant' does not (never added); 'review' cannot be decided from the
|
|
208
|
+
// project yet (never added until someone decides).
|
|
209
|
+
function relevanceOf(rel, facts) {
|
|
210
|
+
const topic = topicOf(rel);
|
|
211
|
+
if (!topic) return { status: 'add', reason: '' };
|
|
212
|
+
if (topic === 'mcp') return { status: 'irrelevant', reason: 'empty MCP template, no servers configured' };
|
|
213
|
+
if (topic.startsWith('tracker:')) {
|
|
214
|
+
const tracker = topic.slice('tracker:'.length);
|
|
215
|
+
if (facts.trackers === null) {
|
|
216
|
+
return { status: 'review', reason: `tracker not detected; set "trackers" in ${RETIRED_FILE}` };
|
|
217
|
+
}
|
|
218
|
+
if (facts.trackers.includes(tracker)) return { status: 'add', reason: '' };
|
|
219
|
+
return { status: 'irrelevant', reason: `project tracker is ${facts.trackers.join(', ') || 'none'}` };
|
|
220
|
+
}
|
|
221
|
+
const detected = { playwright: facts.playwright, prisma: facts.prisma, ui: facts.ui }[topic];
|
|
222
|
+
if (detected) return { status: 'add', reason: '' };
|
|
223
|
+
if (topic === 'ui' && !facts.stackKnown) return { status: 'review', reason: 'no stack manifest found' };
|
|
224
|
+
return { status: 'irrelevant', reason: TOPIC_NOTES[topic] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- thin wrappers ----
|
|
228
|
+
|
|
229
|
+
// The wrapper's kind and the sources it must link to: its snippet, then a same-named skill.
|
|
230
|
+
function wrapperSources(rel) {
|
|
231
|
+
const n = rel.split(path.sep).join('/');
|
|
232
|
+
const kind = WRAPPER_KINDS.find((k) => n.startsWith(k.dir + '/') && !n.slice(k.dir.length + 1).includes('/'));
|
|
233
|
+
if (!kind) return null;
|
|
234
|
+
const base = path.posix.basename(n);
|
|
235
|
+
const name = base.replace(/\.(agent|instructions|prompt)\.md$/, '').replace(/\.mdc$/, '').replace(/\.md$/, '');
|
|
236
|
+
const snippet = `.claude/${kind.snippets}/${name}.md`;
|
|
237
|
+
const skill = `.agents/skills/${name}/SKILL.md`;
|
|
238
|
+
return [{ rel: snippet, needle: `${kind.snippets}/${name}.md` }, { rel: skill, needle: skill }];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function bodyOf(content) {
|
|
242
|
+
if (!content.startsWith('---\n')) return content;
|
|
243
|
+
const end = content.indexOf('\n---', 3); // the closing delimiter; empty frontmatter ("---\n---") included
|
|
244
|
+
return end < 0 ? content : content.slice(end + 4);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// `targets` are the sources that exist in the project, each as { rel, needle }.
|
|
248
|
+
// thin: links a source, with at most one short summary line, a few key points and no table or other policy
|
|
249
|
+
// text; inlined: more prose, a table or too many key points outside the link; diverged: links no existing source.
|
|
250
|
+
function auditWrapper(content, targets) {
|
|
251
|
+
const linked = targets.find((t) => bodyOf(content).includes(t.needle));
|
|
252
|
+
if (!linked) return { status: 'diverged', reason: `does not link ${targets.map((t) => t.rel).join(' or ')}` };
|
|
253
|
+
const prose = [];
|
|
254
|
+
let bullets = 0;
|
|
255
|
+
// "Agent Dispatch" names the agent to activate and "Arguments" the command syntax: both are activation metadata.
|
|
256
|
+
let metadata = false;
|
|
257
|
+
for (const raw of bodyOf(content).split('\n')) {
|
|
258
|
+
const line = raw.trim();
|
|
259
|
+
if (/^#+\s/.test(line)) { metadata = /^#+\s+(agent dispatch|arguments)\b/i.test(line); continue; }
|
|
260
|
+
if (metadata || line === '$ARGUMENTS') continue;
|
|
261
|
+
if (!line || line.startsWith('>') || line.includes(linked.needle)) continue;
|
|
262
|
+
if (/^([-*]|\d+\.)\s/.test(line)) { bullets++; continue; }
|
|
263
|
+
if (/:$/.test(line) && line.split(/\s+/).length <= 3) continue; // a label such as "Key points:"
|
|
264
|
+
prose.push(line);
|
|
265
|
+
}
|
|
266
|
+
const summaryOnly = prose.length === 1 && prose[0].split(/\s+/).length <= MAX_SUMMARY_WORDS;
|
|
267
|
+
if (prose.length > 0 && !summaryOnly) {
|
|
268
|
+
return { status: 'inlined', reason: `${prose.length} line(s) of policy text or table outside the link` };
|
|
269
|
+
}
|
|
270
|
+
if (bullets > MAX_KEY_POINTS) {
|
|
271
|
+
return { status: 'inlined', reason: `${bullets} key points (max ${MAX_KEY_POINTS}); move the rest into the source` };
|
|
272
|
+
}
|
|
273
|
+
return { status: 'thin', reason: '' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Wrappers among the planned entries that exist in the project and are not thin. Read-only: the caller
|
|
277
|
+
// reports them and proposes moving the inlined content into the snippet.
|
|
278
|
+
function auditWrappers(cwd, plan) {
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const e of plan) {
|
|
281
|
+
if (e.action === 'retired') continue;
|
|
282
|
+
const sources = wrapperSources(e.rel);
|
|
283
|
+
const file = path.join(cwd, e.rel);
|
|
284
|
+
if (!sources || !fs.existsSync(file)) continue;
|
|
285
|
+
const targets = sources.filter((s) => fs.existsSync(path.join(cwd, s.rel)));
|
|
286
|
+
const result = targets.length
|
|
287
|
+
? auditWrapper(fs.readFileSync(file, 'utf8'), targets)
|
|
288
|
+
: { status: 'diverged', reason: `no ${sources[0].rel} or ${sources[1].rel} to link` };
|
|
289
|
+
if (result.status !== 'thin') out.push({ rel: e.rel, status: result.status, reason: result.reason });
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
RETIRED_FILE,
|
|
296
|
+
TRACKERS,
|
|
297
|
+
MAX_KEY_POINTS,
|
|
298
|
+
readRetired,
|
|
299
|
+
retiredFor,
|
|
300
|
+
detectProject,
|
|
301
|
+
topicOf,
|
|
302
|
+
relevanceOf,
|
|
303
|
+
wrapperSources,
|
|
304
|
+
auditWrapper,
|
|
305
|
+
auditWrappers,
|
|
306
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitm-ai-dev-toolkit",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.3",
|
|
4
4
|
"description": "Scaffold AI agent config (Cursor, GitHub Copilot, Claude Code, OpenCode, Codex) into any repo, then hand off to your AI agent to finish setup.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nitm-ai-dev-toolkit": "bin/cli.js"
|
|
@@ -294,7 +294,8 @@ If a ticket cannot be created, stop at `awaiting-plan`. Report the lines that ha
|
|
|
294
294
|
The default build location. Follow the [using-git-worktrees](../using-git-worktrees/SKILL.md) skill. Factory always takes its project-local `.worktrees/` default and never the global `~/.worktrees` option, without asking.
|
|
295
295
|
|
|
296
296
|
- Branch `factory/<id>` from `{{DEFAULT_BRANCH}}`.
|
|
297
|
-
- Path `.worktrees/<id>/` at the root of the main checkout (the git root, inside the repo folder). Record its absolute path in `worktree_path`.
|
|
297
|
+
- Path `.worktrees/<id>/` at the root of the main checkout (the git root, inside the repo folder). The folder is named by the job id, never by the branch: branch `factory/<id>` must not produce `.worktrees/factory/<id>`. Record its absolute path in `worktree_path`.
|
|
298
|
+
- `factory/worktree-bootstrap.md` replaces the worktree skill's project-setup and baseline-test steps. Factory does not run those.
|
|
298
299
|
- Before the first worktree, run that skill's `.gitignore` check. If it had to add `.worktrees/`, commit that on `{{DEFAULT_BRANCH}}` or tell the operator to.
|
|
299
300
|
- Create it at the start of the build stage, after the plan gate and tracking. Reuse it if it exists (rework and repair).
|
|
300
301
|
- Copy the job's `spec.md`, `plan.md`, and `tasks.md` (if any) from the main checkout's feature folder into the same relative path in the worktree. The builder ticks `tasks.md` there. Then run the bootstrap (next subsection) and make the feature docs commit.
|
|
@@ -37,11 +37,10 @@ Follow this priority order:
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
# Check in priority order
|
|
40
|
-
ls -d .worktrees 2>/dev/null
|
|
41
|
-
ls -d worktrees 2>/dev/null # Alternative
|
|
40
|
+
ls -d .worktrees 2>/dev/null
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
**If found:** Use that directory.
|
|
43
|
+
**If found:** Use that directory.
|
|
45
44
|
|
|
46
45
|
### 2. Check Project Configuration
|
|
47
46
|
|
|
@@ -67,21 +66,21 @@ Which would you prefer?
|
|
|
67
66
|
|
|
68
67
|
## Safety Verification
|
|
69
68
|
|
|
70
|
-
### For Project-Local Directories (.worktrees
|
|
69
|
+
### For Project-Local Directories (.worktrees)
|
|
71
70
|
|
|
72
71
|
**MUST verify directory is ignored before creating worktree:**
|
|
73
72
|
|
|
74
73
|
```bash
|
|
75
74
|
# Check if directory is ignored (respects local, global, and system gitignore)
|
|
76
|
-
git check-ignore -q .worktrees 2>/dev/null
|
|
75
|
+
git check-ignore -q .worktrees 2>/dev/null
|
|
77
76
|
```
|
|
78
77
|
|
|
79
78
|
**If NOT ignored:**
|
|
80
79
|
|
|
81
80
|
Fix immediately before proceeding:
|
|
82
81
|
|
|
83
|
-
1. Add
|
|
84
|
-
2. Commit the change
|
|
82
|
+
1. Add `.worktrees/` to .gitignore
|
|
83
|
+
2. Commit the change on the default branch
|
|
85
84
|
3. Proceed with worktree creation
|
|
86
85
|
|
|
87
86
|
**Why critical:** Prevents accidentally committing worktree contents to repository.
|
|
@@ -103,11 +102,11 @@ project=$(basename "$(git rev-parse --show-toplevel)")
|
|
|
103
102
|
```bash
|
|
104
103
|
# Determine full path
|
|
105
104
|
case $LOCATION in
|
|
106
|
-
.worktrees
|
|
105
|
+
.worktrees)
|
|
107
106
|
path="$LOCATION/$BRANCH_NAME"
|
|
108
107
|
;;
|
|
109
108
|
~/.worktrees/*)
|
|
110
|
-
path="
|
|
109
|
+
path="$HOME/.worktrees/$project/$BRANCH_NAME"
|
|
111
110
|
;;
|
|
112
111
|
esac
|
|
113
112
|
|
|
@@ -164,8 +163,6 @@ Ready to implement <feature-name>
|
|
|
164
163
|
| Situation | Action |
|
|
165
164
|
| -------------------------- | ------------------------------- |
|
|
166
165
|
| `.worktrees/` exists | Use it (verify ignored) |
|
|
167
|
-
| `worktrees/` exists | Use it (verify ignored) |
|
|
168
|
-
| Both exist | Use `.worktrees/` |
|
|
169
166
|
| Neither exists | Check project config → Ask user |
|
|
170
167
|
| Directory not ignored | Add to .gitignore + commit |
|
|
171
168
|
| Tests fail during baseline | Report failures + ask |
|
|
@@ -115,11 +115,13 @@ The CLI runs the same patch flow without an AI session: `npx nitm-ai-dev-toolkit
|
|
|
115
115
|
|
|
116
116
|
For each category, compare template files against project files:
|
|
117
117
|
|
|
118
|
-
a. **Classify each file
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
118
|
+
a. **Classify each file** (relevance-aware; never a wholesale copy). `npx nitm-ai-dev-toolkit patch --dry-run` makes this classification and prints each reason. Read that plan before deciding anything, and see Relevance and Retirement in `bootstrap.md`:
|
|
119
|
+
- **add**: missing, and it applies to this project (tracker, Playwright, Prisma, UI, and MCP relevance)
|
|
120
|
+
- **update**: the toolkit wrote it and the project has not changed it since; the new template replaces it
|
|
121
|
+
- **current**: identical to the template; no action
|
|
122
|
+
- **needs review**: differs from the template (locally changed), or missing while relevance is undecided; never overwritten, and never added without a decision
|
|
123
|
+
- **not relevant**: does not apply to this project; never added. A present copy is a retirement candidate
|
|
124
|
+
- **retired**: listed in `.nitm/retired.json`; never added, updated or overwritten
|
|
123
125
|
- **⚠️ Missing in Template**: Exists in project but not in template → project-specific, keep as-is except the approved obsolete toolkit Ponytail removal above
|
|
124
126
|
|
|
125
127
|
b. **For updated files, generate a structured diff**:
|
|
@@ -130,7 +132,7 @@ The CLI runs the same patch flow without an AI session: `npx nitm-ai-dev-toolkit
|
|
|
130
132
|
|
|
131
133
|
c. **Smart Merge Rules**:
|
|
132
134
|
- **Never overwrite** project-specific values that replaced `{{PLACEHOLDER}}`
|
|
133
|
-
- **
|
|
135
|
+
- **Add only** files classified *add*. Never add a not-relevant or retired file, and never add a file whose relevance is undecided without the operator's decision
|
|
134
136
|
- **Merge structural additions** into existing customized files
|
|
135
137
|
- **Flag for review** any removals from files that have been customized
|
|
136
138
|
- **Preserve custom agents/skills** that were added by the project
|
|
@@ -144,13 +146,13 @@ The CLI runs the same patch flow without an AI session: `npx nitm-ai-dev-toolkit
|
|
|
144
146
|
**Project Type**: repo | monorepo
|
|
145
147
|
**Categories Scanned**: [list]
|
|
146
148
|
|
|
147
|
-
### 🆕
|
|
149
|
+
### 🆕 Add (X files; apply to this project)
|
|
148
150
|
|
|
149
151
|
| File | Category | Description |
|
|
150
152
|
| ---------------------------- | -------- | ------------------------ |
|
|
151
153
|
| `.cursor/rules/new-rule.mdc` | Rules | New coding standard rule |
|
|
152
154
|
|
|
153
|
-
### 📝
|
|
155
|
+
### 📝 Update (X files; unchanged since the toolkit wrote them)
|
|
154
156
|
|
|
155
157
|
| File | Changes | Risk |
|
|
156
158
|
| -------------------------------------------------------- | ------------------------------ | -------------- |
|
|
@@ -161,16 +163,30 @@ The CLI runs the same patch flow without an AI session: `npx nitm-ai-dev-toolkit
|
|
|
161
163
|
|
|
162
164
|
[No action needed]
|
|
163
165
|
|
|
164
|
-
### 🔧
|
|
166
|
+
### 🔧 Needs Review (X files; locally changed or undecided; not written)
|
|
165
167
|
|
|
166
168
|
| File | Template Changes | Project Customizations | Merge Strategy |
|
|
167
169
|
| ----------- | ----------------- | ---------------------------- | -------------- |
|
|
168
170
|
| `AGENTS.md` | New section added | Has project-specific content | Manual merge |
|
|
169
171
|
|
|
172
|
+
### 🚫 Not Relevant or Retired (X files; not written)
|
|
173
|
+
|
|
174
|
+
| File | Reason | Action |
|
|
175
|
+
| ------------------------------------- | ---------------------------- | --------------------------------------- |
|
|
176
|
+
| `.claude/commands/playwright-test.md` | No Playwright setup detected | Keep out; retire if present (approval) |
|
|
177
|
+
| `.agents/skills/linear-cli/SKILL.md` | Project tracker is GitHub | Retire in `.nitm/retired.json` |
|
|
178
|
+
|
|
179
|
+
### 🩹 Harness Wrappers to Review (X files; proposals only)
|
|
180
|
+
|
|
181
|
+
| Wrapper | Finding | Proposal |
|
|
182
|
+
| ----------------------------- | ------------------------------------ | ------------------------------------------------ |
|
|
183
|
+
| `.claude/rules/api-server.md` | Policy text outside the snippet link | Move the text into its snippet; keep a thin link |
|
|
184
|
+
|
|
170
185
|
### Summary
|
|
171
186
|
|
|
172
|
-
- **Auto-apply**: X
|
|
173
|
-
- **Review needed**: Z
|
|
187
|
+
- **Auto-apply**: X add + Y update entries, as printed by `patch`
|
|
188
|
+
- **Review needed**: Z needs-review files and W wrapper proposals
|
|
189
|
+
- **Not written**: N not-relevant or retired entries
|
|
174
190
|
- **No action**: W files already current
|
|
175
191
|
```
|
|
176
192
|
|
|
@@ -178,9 +194,9 @@ The CLI runs the same patch flow without an AI session: `npx nitm-ai-dev-toolkit
|
|
|
178
194
|
- If `--dry-run` flag: Output the report without making changes
|
|
179
195
|
- Otherwise: Present the report and ask the user to confirm with (Y/n)
|
|
180
196
|
- On confirmation:
|
|
181
|
-
a. **
|
|
182
|
-
b. **
|
|
183
|
-
c. **
|
|
197
|
+
a. **Add and update**: run `npx nitm-ai-dev-toolkit patch`. It writes only the add and update entries it printed; do not copy template files by hand
|
|
198
|
+
b. **Retirements**: after approval, record each path in `.nitm/retired.json` with a reason and date. Do not delete files to retire them
|
|
199
|
+
c. **Needs review**: propose a merge per file (structural additions, preserved project values) and apply it only after the operator approves that file. Wrappers that inline policy: propose moving the content into the snippet and reducing the wrapper to a thin link (see Thin Wrapper Policy in `bootstrap.md`)
|
|
184
200
|
d. **OpenCode plugins**: Apply only the approved obsolete Ponytail cleanup above after backing up the known obsolete toolkit copies and local config; preserve all unrelated local/global plugins. Never add Ponytail to project config.
|
|
185
201
|
e. **Toolkit version**: Keep the `.toolkit-version` value the CLI writes for the verified target version (the installed package version, 4.0.0 or later; never hard-coded) only after the approved migration and OpenCode preflight pass (or OpenCode is confirmed out of scope).
|
|
186
202
|
f. **Git commit**: Stage and commit all changes with message:
|
|
@@ -255,6 +255,8 @@ Follow the rules defined in [.claude/rules-snippets/{{NAME}}.md](../../.claude/r
|
|
|
255
255
|
|
|
256
256
|
### Step 7: Create .opencode/ Directory (confirmed OpenCode use only)
|
|
257
257
|
|
|
258
|
+
Create wrappers below only for entries the patch plan classifies *add* (never retired or not-relevant snippets). Each wrapper stays thin (see Thin Wrapper Policy in `bootstrap.md`).
|
|
259
|
+
|
|
258
260
|
For the v3 route, merge only relevant native OpenCode host fields; preserve existing wrapper bodies, modes, unrelated frontmatter, and shared snippet bodies/frontmatter. The wrapper examples below apply only to approved missing files, not replacement of existing wrappers.
|
|
259
261
|
|
|
260
262
|
1. Merge `.opencode/opencode.jsonc` using native V2 fields:
|
|
@@ -312,12 +314,13 @@ For the v3 route, merge only relevant native OpenCode host fields; preserve exis
|
|
|
312
314
|
|
|
313
315
|
### Step 8: Install Pre-Commit Hook
|
|
314
316
|
|
|
315
|
-
Install the CHANGELOG/secrets enforcement hook
|
|
317
|
+
Install the CHANGELOG/secrets enforcement hook before every commit, only with the operator's approval, only in a git repository, and never over an existing `.git/hooks/pre-commit`. If one exists, show the difference and propose chaining or merging instead:
|
|
316
318
|
|
|
317
319
|
1. Copy `scripts/pre-commit-check.sh` from the template to the target project's `scripts/` directory
|
|
318
320
|
2. Install as a git hook:
|
|
319
321
|
|
|
320
322
|
```bash
|
|
323
|
+
# only when .git/hooks/pre-commit does not exist yet
|
|
321
324
|
cp scripts/pre-commit-check.sh .git/hooks/pre-commit
|
|
322
325
|
chmod +x .git/hooks/pre-commit
|
|
323
326
|
```
|
|
@@ -330,10 +333,12 @@ This enforces:
|
|
|
330
333
|
|
|
331
334
|
Use `git commit --no-verify` to bypass.
|
|
332
335
|
|
|
333
|
-
### Step 9: Install Project Hooks
|
|
336
|
+
### Step 9: Install Project Hooks (only with approval)
|
|
337
|
+
|
|
338
|
+
The `hooks/` files are Claude Code plugin hooks (`${CLAUDE_PLUGIN_ROOT}`) and the Ponytail runtime. They fire only when installed through the Ponytail plugin, not from project settings. Copy only the files the operator approves for a project that uses that plugin, and never overwrite an existing hook file.
|
|
334
339
|
|
|
335
|
-
1. Create `hooks/`
|
|
336
|
-
2. Copy
|
|
340
|
+
1. Create `hooks/` in the target project when copying
|
|
341
|
+
2. Copy the approved files from the template's `hooks/*` directory (preserve sub-structure); skip any file that already exists
|
|
337
342
|
3. Make `.js` files executable on Unix: `chmod +x hooks/*.js`
|
|
338
343
|
|
|
339
344
|
### Step 10: Clean Up Duplicate Skills
|