@antomanc/aginit 0.4.1 → 0.5.1
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 +39 -0
- package/README.md +116 -34
- package/THIRD_PARTY_NOTICES.md +92 -16
- package/dist/adapters/agent-docs.d.ts +35 -0
- package/dist/adapters/agent-docs.d.ts.map +1 -0
- package/dist/adapters/agent-docs.js +156 -0
- package/dist/adapters/agent-docs.js.map +1 -0
- package/dist/adapters/agents-md.d.ts +15 -1
- package/dist/adapters/agents-md.d.ts.map +1 -1
- package/dist/adapters/agents-md.js +146 -120
- package/dist/adapters/agents-md.js.map +1 -1
- package/dist/adapters/e2e.d.ts +15 -0
- package/dist/adapters/e2e.d.ts.map +1 -0
- package/dist/adapters/e2e.js +55 -0
- package/dist/adapters/e2e.js.map +1 -0
- package/dist/adapters/skills.d.ts +5 -0
- package/dist/adapters/skills.d.ts.map +1 -1
- package/dist/adapters/skills.js +17 -0
- package/dist/adapters/skills.js.map +1 -1
- package/dist/cli/capabilities.d.ts +17 -0
- package/dist/cli/capabilities.d.ts.map +1 -0
- package/dist/cli/capabilities.js +149 -0
- package/dist/cli/capabilities.js.map +1 -0
- package/dist/cli/doctor.d.ts +16 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +90 -70
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +117 -20
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/new.d.ts +15 -1
- package/dist/cli/new.d.ts.map +1 -1
- package/dist/cli/new.js +157 -68
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/wizard.d.ts +5 -0
- package/dist/cli/wizard.d.ts.map +1 -1
- package/dist/cli/wizard.js +104 -102
- package/dist/cli/wizard.js.map +1 -1
- package/dist/config/defaults.d.ts +14 -6
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +38 -174
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/schema.d.ts +14 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/validation.d.ts +1 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +63 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/detect/index.d.ts +18 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +120 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/presets/cli.d.ts.map +1 -1
- package/dist/presets/cli.js +7 -0
- package/dist/presets/cli.js.map +1 -1
- package/dist/presets/generic.d.ts.map +1 -1
- package/dist/presets/generic.js +4 -1
- package/dist/presets/generic.js.map +1 -1
- package/dist/presets/testing.d.ts +1 -1
- package/dist/presets/testing.d.ts.map +1 -1
- package/dist/presets/testing.js +22 -8
- package/dist/presets/testing.js.map +1 -1
- package/dist/presets/web.d.ts.map +1 -1
- package/dist/presets/web.js +5 -2
- package/dist/presets/web.js.map +1 -1
- package/dist/registry/catalog.d.ts +13 -0
- package/dist/registry/catalog.d.ts.map +1 -0
- package/dist/registry/catalog.js +544 -0
- package/dist/registry/catalog.js.map +1 -0
- package/dist/registry/index.d.ts +84 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +309 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/setup.d.ts +14 -0
- package/dist/registry/setup.d.ts.map +1 -0
- package/dist/registry/setup.js +25 -0
- package/dist/registry/setup.js.map +1 -0
- package/dist/registry/types.d.ts +115 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/package.json +3 -2
- package/src/adapters/agent-docs.ts +197 -0
- package/src/adapters/agents-md.ts +171 -137
- package/src/adapters/e2e.ts +58 -0
- package/src/adapters/skills.ts +18 -0
- package/src/cli/capabilities.ts +176 -0
- package/src/cli/doctor.ts +83 -66
- package/src/cli/init.ts +1 -1
- package/src/cli/main.ts +125 -20
- package/src/cli/new.ts +195 -74
- package/src/cli/wizard.ts +130 -103
- package/src/config/defaults.ts +57 -179
- package/src/config/schema.ts +15 -0
- package/src/config/validation.ts +56 -0
- package/src/detect/index.ts +134 -0
- package/src/index.ts +5 -0
- package/src/presets/cli.ts +6 -0
- package/src/presets/generic.ts +4 -1
- package/src/presets/testing.ts +29 -12
- package/src/presets/web.ts +4 -2
- package/src/registry/catalog.ts +569 -0
- package/src/registry/index.ts +395 -0
- package/src/registry/setup.ts +40 -0
- package/src/registry/types.ts +109 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AginitConfig } from '../config/schema.js';
|
|
4
|
+
import { presetPlatform } from '../config/defaults.js';
|
|
5
|
+
import { detectProject, projectCommands } from '../detect/index.js';
|
|
6
|
+
import {
|
|
7
|
+
activeCapabilities,
|
|
8
|
+
CapabilityDescriptor,
|
|
9
|
+
CATALOG,
|
|
10
|
+
e2eDirFor,
|
|
11
|
+
GuideContext,
|
|
12
|
+
MicroDoc,
|
|
13
|
+
Platform,
|
|
14
|
+
Stack
|
|
15
|
+
} from '../registry/index.js';
|
|
16
|
+
import { readTextFile, safeWriteFile } from '../utils/fs.js';
|
|
17
|
+
import { logger } from '../utils/logger.js';
|
|
18
|
+
|
|
19
|
+
export interface ProjectProfile extends GuideContext {
|
|
20
|
+
active: CapabilityDescriptor[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Platform and stack: saved in v2 configs, detected for older ones. */
|
|
24
|
+
export function resolvePlatformStack(
|
|
25
|
+
config: AginitConfig,
|
|
26
|
+
targetDir?: string
|
|
27
|
+
): { platform: Platform; stack: Stack } {
|
|
28
|
+
const detected = targetDir && (!config.platform || !config.stack) ? detectProject(targetDir) : null;
|
|
29
|
+
return {
|
|
30
|
+
platform:
|
|
31
|
+
config.platform ??
|
|
32
|
+
(config.preset !== 'generic' ? presetPlatform(config.preset) : (detected?.platform ?? 'cli')),
|
|
33
|
+
stack: config.stack ?? (detected && detected.stack !== 'unknown' ? detected.stack : 'node')
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function projectProfile(
|
|
38
|
+
config: AginitConfig,
|
|
39
|
+
options: { targetDir?: string; scripts?: Record<string, string> } = {}
|
|
40
|
+
): ProjectProfile {
|
|
41
|
+
const { platform, stack } = resolvePlatformStack(config, options.targetDir);
|
|
42
|
+
const active = activeCapabilities(config);
|
|
43
|
+
const ids = new Set(active.map((c) => c.id));
|
|
44
|
+
return {
|
|
45
|
+
platform,
|
|
46
|
+
stack,
|
|
47
|
+
active,
|
|
48
|
+
has: (id) => ids.has(id),
|
|
49
|
+
commands: projectCommands(options.targetDir ?? '.', stack, {
|
|
50
|
+
packageManager: config.packageManager,
|
|
51
|
+
scripts: options.scripts
|
|
52
|
+
}),
|
|
53
|
+
e2eDir: e2eDirFor(stack)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const DOCS_DIR = path.join('docs', 'agents');
|
|
58
|
+
export const GENERATED_MARKER =
|
|
59
|
+
'<!-- aginit:managed — generated by npx @antomanc/aginit. Delete this line to take ownership of this file. -->';
|
|
60
|
+
export const LEGACY_GENERATED_MARKER =
|
|
61
|
+
'<!-- Generated by aginit from aginit.config.json. Rerun `aginit` to refresh; delete this line to edit by hand. -->';
|
|
62
|
+
|
|
63
|
+
const DOC_TITLES: Record<MicroDoc, string> = {
|
|
64
|
+
testing: 'Testing',
|
|
65
|
+
'ui-verification': 'Live UI verification',
|
|
66
|
+
workflows: 'Engineering workflows & skills'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const skillNames = (capability: CapabilityDescriptor) =>
|
|
70
|
+
capability.install.flatMap((step) => (step.kind === 'skills-sh' ? step.skills : []));
|
|
71
|
+
|
|
72
|
+
function testingIntro(profile: ProjectProfile): string[] {
|
|
73
|
+
const { commands } = profile;
|
|
74
|
+
const rows: Array<[string, string | undefined]> = [
|
|
75
|
+
['Build', commands.build],
|
|
76
|
+
['Typecheck', commands.typecheck],
|
|
77
|
+
['Lint / analyze', commands.lint],
|
|
78
|
+
['Test', commands.test],
|
|
79
|
+
['Unit test', commands.unitTest],
|
|
80
|
+
['E2E', commands.e2e]
|
|
81
|
+
];
|
|
82
|
+
const lines = rows.filter(([, cmd]) => cmd).map(([label, cmd]) => `- ${label}: \`${cmd}\``);
|
|
83
|
+
return lines.length
|
|
84
|
+
? ['Run the full check before calling a change done:', ...lines]
|
|
85
|
+
: ['Record the project build and test commands here once they exist.'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function workflowsExtras(profile: ProjectProfile): string[] {
|
|
89
|
+
// Skill capabilities without their own guide still show up, so new registry
|
|
90
|
+
// entries and custom skill repos are documented without extra code.
|
|
91
|
+
const plain = profile.active.filter(
|
|
92
|
+
(c) => !c.guide && c.install.some((step) => step.kind === 'skills-sh')
|
|
93
|
+
);
|
|
94
|
+
const bullet = (c: CapabilityDescriptor) => {
|
|
95
|
+
const skills = skillNames(c);
|
|
96
|
+
const label = skills.length ? skills.map((s) => '`' + s + '`').join(', ') : `\`${c.id}\``;
|
|
97
|
+
return `- ${label}: ${c.description}.`;
|
|
98
|
+
};
|
|
99
|
+
const discipline = plain.filter((c) => c.category === 'discipline');
|
|
100
|
+
const others = plain.filter((c) => c.category !== 'discipline');
|
|
101
|
+
return [
|
|
102
|
+
...(discipline.length ? ['## Discipline (apply to every change)', ...discipline.map(bullet)] : []),
|
|
103
|
+
...(others.length ? ['', '## Other skills', ...others.map(bullet)] : [])
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Drivers come before the design review that uses them; unit runners before E2E.
|
|
108
|
+
const SECTION_ORDER = ['methodology', 'ui-driver', 'design', 'testing-engine', 'context-guard'];
|
|
109
|
+
const sectionRank = (c: CapabilityDescriptor) => {
|
|
110
|
+
const category = SECTION_ORDER.indexOf(c.category);
|
|
111
|
+
const index = CATALOG.indexOf(c);
|
|
112
|
+
return (category === -1 ? SECTION_ORDER.length : category) * 1000 + (index === -1 ? 999 : index);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Markdown for one micro-doc, or null when no active capability contributes to it. */
|
|
116
|
+
export function renderMicroDoc(doc: MicroDoc, profile: ProjectProfile): string | null {
|
|
117
|
+
const sections = profile.active
|
|
118
|
+
.filter((c) => c.guide?.doc === doc)
|
|
119
|
+
.sort((a, b) => sectionRank(a) - sectionRank(b))
|
|
120
|
+
.map((c) => c.guide!.render(profile));
|
|
121
|
+
const extras = doc === 'workflows' ? workflowsExtras(profile) : [];
|
|
122
|
+
if (sections.length === 0 && extras.length === 0) return null;
|
|
123
|
+
const intro =
|
|
124
|
+
doc === 'testing'
|
|
125
|
+
? testingIntro(profile)
|
|
126
|
+
: doc === 'workflows'
|
|
127
|
+
? [
|
|
128
|
+
'Skills live in `.agents/skills/` (linked into `.claude/skills/` for Claude Code). Invoke one by name',
|
|
129
|
+
'(for example `/tdd`), or let the agent pick it from its description.'
|
|
130
|
+
]
|
|
131
|
+
: ['Verify UI changes in the running app, not only in tests.'];
|
|
132
|
+
const body = [`# ${DOC_TITLES[doc]}`, '', ...intro, '', ...sections.flatMap((s) => [s, ''])];
|
|
133
|
+
if (extras.length) body.push(...extras, '');
|
|
134
|
+
return [GENERATED_MARKER, ...body].join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const MICRO_DOCS: MicroDoc[] = ['testing', 'ui-verification', 'workflows'];
|
|
138
|
+
|
|
139
|
+
export function microDocPath(doc: MicroDoc): string {
|
|
140
|
+
return path.join(DOCS_DIR, `${doc}.md`).split(path.sep).join('/');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Checks if a micro-doc's content still carries an aginit managed marker. */
|
|
144
|
+
export function isManagedMicroDoc(text: string | null): boolean {
|
|
145
|
+
if (text === null) return true;
|
|
146
|
+
const firstLine = text.split(/\r?\n/, 1)[0].trim();
|
|
147
|
+
return (
|
|
148
|
+
firstLine.startsWith(GENERATED_MARKER) ||
|
|
149
|
+
firstLine.startsWith(LEGACY_GENERATED_MARKER) ||
|
|
150
|
+
/^<!--\s*aginit:managed(?:\b[^>]*)?-->/.test(firstLine) ||
|
|
151
|
+
/^<!--\s*Generated by aginit\b/.test(firstLine)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Docs aginit owns: files it wrote that still carry the generated marker. */
|
|
156
|
+
function ownedByAginit(file: string): boolean {
|
|
157
|
+
return isManagedMicroDoc(readTextFile(file));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Write the docs/agents/ micro-docs for the active capabilities. A doc whose
|
|
162
|
+
* marker line was removed belongs to the user and is never touched; a doc no
|
|
163
|
+
* capability contributes to any more is deleted. Returns the docs that exist.
|
|
164
|
+
*/
|
|
165
|
+
export function setupMicroDocs(
|
|
166
|
+
targetDir: string,
|
|
167
|
+
profile: ProjectProfile,
|
|
168
|
+
options: { dryRun?: boolean; silent?: boolean } = {}
|
|
169
|
+
): MicroDoc[] {
|
|
170
|
+
const { dryRun = false, silent = false } = options;
|
|
171
|
+
const present: MicroDoc[] = [];
|
|
172
|
+
for (const doc of MICRO_DOCS) {
|
|
173
|
+
const file = path.join(targetDir, DOCS_DIR, `${doc}.md`);
|
|
174
|
+
const content = renderMicroDoc(doc, profile);
|
|
175
|
+
if (!ownedByAginit(file)) {
|
|
176
|
+
if (!silent) logger.dim(`${microDocPath(doc)} was edited by hand, preserving it.`);
|
|
177
|
+
present.push(doc);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (content === null) {
|
|
181
|
+
if (fs.existsSync(file)) {
|
|
182
|
+
if (dryRun) logger.dim(`[dry-run] Would remove file: ${file}`);
|
|
183
|
+
else fs.rmSync(file);
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
present.push(doc);
|
|
188
|
+
if (readTextFile(file) !== content)
|
|
189
|
+
safeWriteFile(file, content, { overwrite: true, dryRun, silent });
|
|
190
|
+
}
|
|
191
|
+
return present;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Docs that will exist for a profile (used when rendering AGENTS.md without a target). */
|
|
195
|
+
export function plannedMicroDocs(profile: ProjectProfile): MicroDoc[] {
|
|
196
|
+
return MICRO_DOCS.filter((doc) => renderMicroDoc(doc, profile) !== null);
|
|
197
|
+
}
|
|
@@ -3,97 +3,151 @@ import { fileExists, readTextFile, safeWriteFile } from '../utils/fs.js';
|
|
|
3
3
|
import { AginitConfig } from '../config/schema.js';
|
|
4
4
|
import { readPackageManifest } from '../config/validation.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
-
import {
|
|
6
|
+
import { CapabilityDescriptor, isNodeStack, MicroDoc } from '../registry/index.js';
|
|
7
|
+
import {
|
|
8
|
+
microDocPath,
|
|
9
|
+
plannedMicroDocs,
|
|
10
|
+
projectProfile,
|
|
11
|
+
ProjectProfile,
|
|
12
|
+
resolvePlatformStack
|
|
13
|
+
} from './agent-docs.js';
|
|
14
|
+
|
|
15
|
+
const PLATFORM_LABELS: Record<string, string> = {
|
|
16
|
+
web: 'Web',
|
|
17
|
+
'mobile-native': 'Native mobile',
|
|
18
|
+
'mobile-rn': 'React Native',
|
|
19
|
+
backend: 'Backend',
|
|
20
|
+
cli: 'CLI'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const AGINIT_BLOCK_START =
|
|
24
|
+
'<!-- aginit:start — managed by npx @antomanc/aginit (re-run to refresh) -->';
|
|
25
|
+
export const AGINIT_BLOCK_END = '<!-- aginit:end -->';
|
|
26
|
+
|
|
27
|
+
export const AGINIT_START_PATTERN = /<!--\s*aginit:start(?:\b[^>]*)?-->/;
|
|
28
|
+
export const AGINIT_END_PATTERN = /<!--\s*aginit:end(?:\b[^>]*)?-->/;
|
|
29
|
+
|
|
30
|
+
const BOOTSTRAP_SECTION = [
|
|
31
|
+
'## Bootstrap & First Session',
|
|
32
|
+
'When the user asks to "bootstrap this project" or kicks off a new initiative:',
|
|
33
|
+
'1. Understand product intent and ask 2-3 essential questions to resolve ambiguity.',
|
|
34
|
+
'2. Draft or refine canonical context in `PRODUCT.md` and `DESIGN.md` (or `docs/adr/`).',
|
|
35
|
+
'3. Plan in small verifiable increments and implement test-first using installed skills.'
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Placeholder scripts used when AGENTS.md is rendered without a package.json to read. */
|
|
39
|
+
function presetScripts(config: AginitConfig): Record<string, string> {
|
|
40
|
+
return config.preset === 'generic'
|
|
41
|
+
? { test: 'vitest run' }
|
|
42
|
+
: { build: '', typecheck: '', test: '', dev: '', 'test:e2e': '' };
|
|
43
|
+
}
|
|
7
44
|
|
|
8
|
-
|
|
45
|
+
const table = (head: [string, string], rows: Array<[string, string]>) => [
|
|
46
|
+
`| ${head[0]} | ${head[1]} |`,
|
|
47
|
+
'| --- | --- |',
|
|
48
|
+
...rows.map(([a, b]) => `| ${a} | ${b} |`)
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The part of AGENTS.md aginit keeps in sync with the active capabilities:
|
|
53
|
+
* guardrails, the Task Routing table and the Anti-Rationalization table.
|
|
54
|
+
*/
|
|
55
|
+
export function renderAgentsBlock(
|
|
9
56
|
config: AginitConfig,
|
|
10
|
-
|
|
57
|
+
profile: ProjectProfile,
|
|
58
|
+
docs: MicroDoc[] = plannedMicroDocs(profile)
|
|
11
59
|
): string {
|
|
12
|
-
const commands =
|
|
13
|
-
scripts ??
|
|
14
|
-
(config.preset === 'generic'
|
|
15
|
-
? { test: 'vitest run' }
|
|
16
|
-
: { build: '', typecheck: '', test: '', dev: '', 'test:e2e': '' });
|
|
17
|
-
const hasCommand = (name: string) => Object.hasOwn(commands, name);
|
|
18
|
-
const isWeb = config.preset === 'web';
|
|
19
|
-
const hasBrowser = isWeb && config.browser.playwright;
|
|
20
|
-
const hasVisualAgent = isWeb && config.browser.visualAgent;
|
|
21
|
-
const pm = config.packageManager || 'pnpm';
|
|
22
|
-
const pmAdapter = getPackageManagerAdapter(pm);
|
|
23
|
-
|
|
24
|
-
let devCommand = '';
|
|
25
|
-
if (hasCommand('dev') && isWeb && config.framework && config.framework !== 'none') {
|
|
26
|
-
devCommand = `- Dev Server: \`${pmAdapter.runCmd('dev')}\``;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let testE2E = '';
|
|
30
|
-
if (hasBrowser && hasCommand('test:e2e')) {
|
|
31
|
-
testE2E = `- Test (E2E / Browser): \`${pmAdapter.runCmd('test:e2e')}\``;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// A source with an empty skill list (or `*`) installs every skill it ships.
|
|
35
|
-
const skillInstalled = (skill: string) =>
|
|
36
|
-
config.skills.sources.some(
|
|
37
|
-
(source) =>
|
|
38
|
-
source.skills.length === 0 || source.skills.includes('*') || source.skills.includes(skill)
|
|
39
|
-
);
|
|
40
|
-
const mattSkills = (() => {
|
|
41
|
-
const source = config.skills.sources.find((s) => s.package === 'mattpocock/skills');
|
|
42
|
-
if (!source) return [];
|
|
43
|
-
return source.skills.length ? source.skills : ['*'];
|
|
44
|
-
})();
|
|
45
60
|
const hasSkills = config.skills.sources.length > 0;
|
|
61
|
+
const routing: Array<[string, string]> = [
|
|
62
|
+
['Product intent, scope, target users', '`PRODUCT.md`']
|
|
63
|
+
];
|
|
64
|
+
const order = ['design', 'codebase-graph'];
|
|
65
|
+
const rank = (c: CapabilityDescriptor) => (order.indexOf(c.category) + order.length + 1) % (order.length + 1);
|
|
66
|
+
const direct = profile.active.filter((c) => c.routing).sort((a, b) => rank(a) - rank(b));
|
|
67
|
+
const routeOf = (c: CapabilityDescriptor): [string, string] => [c.routing!.task, c.routing!.read];
|
|
68
|
+
// Design and codebase rows lead, discipline rows follow the micro-docs.
|
|
69
|
+
routing.push(...direct.filter((c) => c.category !== 'discipline').map(routeOf));
|
|
70
|
+
if (docs.includes('testing'))
|
|
71
|
+
routing.push(['Automated unit & E2E testing', '`' + microDocPath('testing') + '`']);
|
|
72
|
+
if (docs.includes('ui-verification'))
|
|
73
|
+
routing.push(['Live UI verification (Web / Mobile)', '`' + microDocPath('ui-verification') + '`']);
|
|
74
|
+
if (docs.includes('workflows'))
|
|
75
|
+
routing.push(['Engineering workflow & skills', '`' + microDocPath('workflows') + '`']);
|
|
76
|
+
routing.push(...direct.filter((c) => c.category === 'discipline').map(routeOf));
|
|
77
|
+
|
|
78
|
+
const rebuttals = new Map<string, string>();
|
|
79
|
+
for (const capability of profile.active)
|
|
80
|
+
for (const { excuse, rule } of capability.rebuttals ?? [])
|
|
81
|
+
if (!rebuttals.has(excuse)) rebuttals.set(excuse, rule);
|
|
82
|
+
const unitExcuse = 'Unit tests pass, so the feature works.';
|
|
83
|
+
if (!rebuttals.has(unitExcuse) && profile.active.some((c) => c.category === 'testing-engine'))
|
|
84
|
+
rebuttals.set(
|
|
85
|
+
unitExcuse,
|
|
86
|
+
'Insufficient. Unit tests do not verify wiring or rendering. Exercise the feature end to end ' +
|
|
87
|
+
`(${profile.commands.e2e && profile.has('playwright') ? '`' + profile.commands.e2e + '`' : 'integration or smoke run'}) before marking complete.`
|
|
88
|
+
);
|
|
46
89
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'- **UI & Design**: Use `impeccable` skill for frontend polish, design critique, and token craft.';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let browserCapability = '';
|
|
78
|
-
if (hasVisualAgent && skillInstalled('agent-browser')) {
|
|
79
|
-
browserCapability =
|
|
80
|
-
'- **Browser Verification**: Use `agent-browser` skill for interactive inspection and visual QA.';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let graftCapability = '';
|
|
84
|
-
if (config.codebase.graft) {
|
|
85
|
-
graftCapability =
|
|
86
|
-
'- **Codebase Graph**: Use `graft ask "<query>" --source` or `graft map` for orientation before raw grep.';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const specCapability = skillInstalled('to-spec')
|
|
90
|
-
? '- **Spec & Tickets Workflow**: `to-spec`, `to-tickets`, `implement-spec`'
|
|
91
|
-
: '';
|
|
90
|
+
const lines = [
|
|
91
|
+
AGINIT_BLOCK_START,
|
|
92
|
+
...(profile.has('karpathy-guidelines')
|
|
93
|
+
? [
|
|
94
|
+
'## Guardrails (karpathy-guidelines)',
|
|
95
|
+
'- **Think before coding**: state assumptions and trade-offs; ask instead of guessing.',
|
|
96
|
+
'- **Simplicity first**: minimum code that solves the problem; no speculative features or abstractions.',
|
|
97
|
+
'- **Surgical changes**: touch only what the task needs; no silent scope creep or drive-by refactors.',
|
|
98
|
+
'- **Goal-driven execution**: define a verifiable success check first, then loop until it passes.',
|
|
99
|
+
''
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
'## Task Routing',
|
|
103
|
+
hasSkills
|
|
104
|
+
? config.agents.claudeCode === false
|
|
105
|
+
? 'Load only what the task needs. Skills live in `.agents/skills/`.'
|
|
106
|
+
: 'Load only what the task needs. Skills live in `.agents/skills/` (linked into `.claude/skills/` for Claude Code).'
|
|
107
|
+
: 'Load only what the task needs. Skills installation is disabled in this configuration.',
|
|
108
|
+
'',
|
|
109
|
+
...table(['Task', 'Read / Use'], routing),
|
|
110
|
+
...(rebuttals.size
|
|
111
|
+
? ['', '## Anti-Rationalization', ...table(['Excuse', 'Rule'], [...rebuttals].map(([e, r]) => [`"${e}"`, r]))]
|
|
112
|
+
: []),
|
|
113
|
+
AGINIT_BLOCK_END
|
|
114
|
+
];
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
92
117
|
|
|
93
|
-
|
|
118
|
+
export function generateAgentsMarkdown(
|
|
119
|
+
config: AginitConfig,
|
|
120
|
+
scripts?: Record<string, string>,
|
|
121
|
+
targetDir?: string
|
|
122
|
+
): string {
|
|
123
|
+
const resolved = resolvePlatformStack(config, targetDir);
|
|
124
|
+
const node = isNodeStack(resolved.stack);
|
|
125
|
+
const profile = projectProfile(config, {
|
|
126
|
+
targetDir,
|
|
127
|
+
scripts: node ? (scripts ?? presetScripts(config)) : undefined
|
|
128
|
+
});
|
|
129
|
+
const { commands } = profile;
|
|
130
|
+
const pm = config.packageManager || 'pnpm';
|
|
131
|
+
const isWeb = profile.platform === 'web';
|
|
132
|
+
const commandLines = [
|
|
133
|
+
node ? `- Package Manager: \`${pm}\`` : undefined,
|
|
134
|
+
commands.dev && isWeb && config.framework && config.framework !== 'none'
|
|
135
|
+
? `- Dev Server: \`${commands.dev}\``
|
|
136
|
+
: undefined,
|
|
137
|
+
commands.build ? `- Build: \`${commands.build}\`` : undefined,
|
|
138
|
+
commands.typecheck ? `- Typecheck: \`${commands.typecheck}\`` : undefined,
|
|
139
|
+
commands.lint ? `- Lint: \`${commands.lint}\`` : undefined,
|
|
140
|
+
commands.test ? `- Test: \`${commands.test}\`` : undefined,
|
|
141
|
+
commands.unitTest ? `- Unit Test: \`${commands.unitTest}\`` : undefined,
|
|
142
|
+
commands.e2e && profile.has('playwright')
|
|
143
|
+
? `- Test (E2E / Browser): \`${commands.e2e}\``
|
|
144
|
+
: undefined
|
|
145
|
+
].filter((line): line is string => line !== undefined);
|
|
146
|
+
|
|
147
|
+
const sections: string[] = [
|
|
94
148
|
`# ${config.name}`,
|
|
95
149
|
'',
|
|
96
|
-
`AI-first ${config.preset} project configured with \`aginit
|
|
150
|
+
`AI-first ${config.preset} project configured with \`aginit\` (${PLATFORM_LABELS[profile.platform]}, ${profile.stack}).`,
|
|
97
151
|
'',
|
|
98
152
|
'## Core Invariants',
|
|
99
153
|
'- Keep changes small, focused, and test-verified.',
|
|
@@ -101,80 +155,60 @@ export function generateAgentsMarkdown(
|
|
|
101
155
|
'- Never commit broken builds, failed typechecks, or unverified changes.',
|
|
102
156
|
'',
|
|
103
157
|
'## Essential Commands',
|
|
104
|
-
|
|
105
|
-
devCommand || undefined,
|
|
106
|
-
hasCommand('build') ? `- Build: \`${pmAdapter.runCmd('build')}\`` : undefined,
|
|
107
|
-
hasCommand('typecheck') ? `- Typecheck: \`${pmAdapter.runCmd('typecheck')}\`` : undefined,
|
|
108
|
-
hasCommand('test') ? `- Test: \`${pmAdapter.runCmd('test')}\`` : undefined,
|
|
109
|
-
hasCommand('test:unit') ? `- Unit Test: \`${pmAdapter.runCmd('test:unit')}\`` : undefined,
|
|
110
|
-
testE2E || undefined,
|
|
158
|
+
...(commandLines.length ? commandLines : ['- Add the build and test commands here once they exist.']),
|
|
111
159
|
'',
|
|
112
|
-
|
|
113
|
-
hasSkills
|
|
114
|
-
? config.agents.claudeCode === false
|
|
115
|
-
? 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex):'
|
|
116
|
-
: 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex, linked into `.claude/skills/` for Claude Code):'
|
|
117
|
-
: 'Skills installation is disabled in this configuration.',
|
|
118
|
-
engineeringCapability || undefined,
|
|
119
|
-
proseCapability || undefined,
|
|
120
|
-
simplicityCapability || undefined,
|
|
121
|
-
specCapability || undefined,
|
|
122
|
-
uiCapability || undefined,
|
|
123
|
-
browserCapability || undefined,
|
|
124
|
-
graftCapability || undefined,
|
|
160
|
+
renderAgentsBlock(config, profile),
|
|
125
161
|
'',
|
|
126
|
-
|
|
127
|
-
'When the user asks to "bootstrap this project" or kicks off a new initiative:',
|
|
128
|
-
'1. Understand product intent and ask 2-3 essential questions to resolve ambiguity.',
|
|
129
|
-
'2. Draft or refine canonical context in `PRODUCT.md` and `DESIGN.md` (or `docs/adr/`).',
|
|
130
|
-
'3. Plan in small verifiable increments and implement test-first using installed skills.',
|
|
162
|
+
...BOOTSTRAP_SECTION,
|
|
131
163
|
''
|
|
132
164
|
];
|
|
165
|
+
return sections.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
166
|
+
}
|
|
133
167
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
/** Insert or refresh the aginit block in an existing AGENTS.md, leaving everything else as written. */
|
|
169
|
+
export function mergeAgentsBlock(existing: string, block: string): string {
|
|
170
|
+
const startMatch = existing.match(AGINIT_START_PATTERN);
|
|
171
|
+
if (startMatch && startMatch.index !== undefined) {
|
|
172
|
+
const afterStart = existing.slice(startMatch.index + startMatch[0].length);
|
|
173
|
+
const endMatch = afterStart.match(AGINIT_END_PATTERN);
|
|
174
|
+
if (endMatch && endMatch.index !== undefined) {
|
|
175
|
+
const endOffset = startMatch.index + startMatch[0].length + endMatch.index + endMatch[0].length;
|
|
176
|
+
return existing.slice(0, startMatch.index) + block + existing.slice(endOffset);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Above Graft's block when there is one, so Graft keeps owning the end of the file.
|
|
180
|
+
const graftMatch = existing.match(/<!--\s*graft:start(?:\b[^>]*)?-->/);
|
|
181
|
+
if (graftMatch && graftMatch.index !== undefined) {
|
|
182
|
+
const graft = graftMatch.index;
|
|
183
|
+
return existing.slice(0, graft).trimEnd() + '\n\n' + block + '\n\n' + existing.slice(graft);
|
|
184
|
+
}
|
|
185
|
+
return existing.trimEnd() + '\n\n' + block + '\n';
|
|
142
186
|
}
|
|
143
187
|
|
|
144
188
|
export function setupAgentsMarkdown(
|
|
145
189
|
targetDir: string,
|
|
146
190
|
config: AginitConfig,
|
|
147
|
-
options: { overwrite?: boolean; dryRun?: boolean; silent?: boolean } = {}
|
|
191
|
+
options: { overwrite?: boolean; dryRun?: boolean; silent?: boolean; docs?: MicroDoc[] } = {}
|
|
148
192
|
): boolean {
|
|
149
193
|
const { overwrite = false, dryRun = false, silent = false } = options;
|
|
150
194
|
const filePath = path.join(targetDir, 'AGENTS.md');
|
|
151
195
|
|
|
152
196
|
if (fileExists(filePath) && !overwrite) {
|
|
153
|
-
// File exists; check if it has the bootstrap guidance. If not, append it conservatively.
|
|
154
197
|
const existing = readTextFile(filePath) || '';
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'3. Plan in small verifiable increments and implement test-first using installed skills.',
|
|
163
|
-
''
|
|
164
|
-
].join('\n');
|
|
165
|
-
safeWriteFile(filePath, existing.trimEnd() + '\n' + addition, {
|
|
166
|
-
overwrite: true,
|
|
167
|
-
dryRun,
|
|
168
|
-
silent
|
|
169
|
-
});
|
|
170
|
-
if (!silent) logger.dim('Appended Bootstrap section to existing AGENTS.md');
|
|
198
|
+
const scripts = readPackageManifest(targetDir)?.scripts;
|
|
199
|
+
const profile = projectProfile(config, { targetDir, scripts });
|
|
200
|
+
let updated = mergeAgentsBlock(existing, renderAgentsBlock(config, profile, options.docs));
|
|
201
|
+
if (!updated.includes('## Bootstrap & First Session'))
|
|
202
|
+
updated = updated.trimEnd() + '\n\n' + BOOTSTRAP_SECTION.join('\n') + '\n';
|
|
203
|
+
if (updated === existing) {
|
|
204
|
+
if (!silent) logger.dim('AGENTS.md is up to date, preserving content.');
|
|
171
205
|
return true;
|
|
172
206
|
}
|
|
173
|
-
if (!silent) logger.dim('
|
|
174
|
-
return true;
|
|
207
|
+
if (!silent) logger.dim('Refreshed the aginit section of the existing AGENTS.md');
|
|
208
|
+
return safeWriteFile(filePath, updated, { overwrite: true, dryRun, silent });
|
|
175
209
|
}
|
|
176
210
|
|
|
177
|
-
const content = generateAgentsMarkdown(config, readPackageManifest(targetDir)?.scripts);
|
|
211
|
+
const content = generateAgentsMarkdown(config, readPackageManifest(targetDir)?.scripts, targetDir);
|
|
178
212
|
return safeWriteFile(filePath, content, { overwrite: true, dryRun, silent });
|
|
179
213
|
}
|
|
180
214
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileExists } from '../utils/fs.js';
|
|
3
|
+
import { runExecutable } from '../utils/shell.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { e2eDirFor, Platform, Stack } from '../registry/index.js';
|
|
6
|
+
|
|
7
|
+
const E2E_CONFIGS = ['e2e.config.ts', 'e2e.config.mts', 'e2e.config.js', 'e2e.config.mjs'];
|
|
8
|
+
|
|
9
|
+
export function e2eConfigured(targetDir: string, stack: Stack): boolean {
|
|
10
|
+
const dir = path.join(targetDir, e2eDirFor(stack));
|
|
11
|
+
return E2E_CONFIGS.some((name) => fileExists(path.join(dir, name)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scaffold tester-army/e2e with its own `e2e init`: at the project root for
|
|
16
|
+
* Node projects, in an isolated `.agents/e2e/` workspace otherwise so the root
|
|
17
|
+
* never gets a package.json. The e2e skill comes from the Skills CLI, so init
|
|
18
|
+
* installs no skill or MCP entry; dependencies are installed by the user.
|
|
19
|
+
*/
|
|
20
|
+
export async function setupE2e(
|
|
21
|
+
targetDir: string,
|
|
22
|
+
options: { stack: Stack; platform: Platform; dryRun?: boolean; silent?: boolean }
|
|
23
|
+
): Promise<boolean> {
|
|
24
|
+
const { stack, platform, dryRun = false, silent = false } = options;
|
|
25
|
+
if (e2eConfigured(targetDir, stack)) {
|
|
26
|
+
if (!silent) logger.dim('e2e is already configured, preserving it.');
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const dir = e2eDirFor(stack);
|
|
30
|
+
const engine = platform === 'mobile-native' || platform === 'mobile-rn' ? 'mobile' : 'web';
|
|
31
|
+
const args = [
|
|
32
|
+
'-y',
|
|
33
|
+
'e2e@latest',
|
|
34
|
+
'init',
|
|
35
|
+
...(dir ? [dir] : []),
|
|
36
|
+
'--yes',
|
|
37
|
+
'--engine',
|
|
38
|
+
engine,
|
|
39
|
+
'--skills',
|
|
40
|
+
'none',
|
|
41
|
+
'--mcp',
|
|
42
|
+
'none',
|
|
43
|
+
'--no-install'
|
|
44
|
+
];
|
|
45
|
+
if (!silent) logger.dim(`Scaffolding e2e (${engine} engine) in ${dir || 'the project root'}...`);
|
|
46
|
+
const res = await runExecutable('npx', args, {
|
|
47
|
+
cwd: targetDir,
|
|
48
|
+
dryRun,
|
|
49
|
+
silent: true,
|
|
50
|
+
timeout: 600_000
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
logger.warn(`e2e init failed: ${res.stderr || res.stdout}`);
|
|
54
|
+
logger.dim(` Retry manually: npx e2e init ${dir} --engine ${engine}`.replace(/\s+/g, ' '));
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
package/src/adapters/skills.ts
CHANGED
|
@@ -162,3 +162,21 @@ export async function updateSkills(
|
|
|
162
162
|
});
|
|
163
163
|
return res.ok;
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
/** Remove skills from every agent through the Skills CLI (`npx skills remove`). */
|
|
167
|
+
export async function removeSkills(
|
|
168
|
+
targetDir: string,
|
|
169
|
+
skills: string[],
|
|
170
|
+
options: { dryRun?: boolean; silent?: boolean } = {}
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
if (skills.length === 0) return true;
|
|
173
|
+
for (const skill of skills) validateUpstreamArgument(skill, 'skill name');
|
|
174
|
+
if (!options.silent) logger.dim(`Removing skills: ${skills.join(', ')}...`);
|
|
175
|
+
const res = await runExecutable('npx', ['-y', 'skills', 'remove', ...skills, '-y'], {
|
|
176
|
+
cwd: targetDir,
|
|
177
|
+
dryRun: options.dryRun,
|
|
178
|
+
silent: true
|
|
179
|
+
});
|
|
180
|
+
if (!res.ok) logger.warn(`Failed removing skills: ${res.stderr || res.stdout}`);
|
|
181
|
+
return res.ok;
|
|
182
|
+
}
|