@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +116 -34
  3. package/THIRD_PARTY_NOTICES.md +92 -16
  4. package/dist/adapters/agent-docs.d.ts +35 -0
  5. package/dist/adapters/agent-docs.d.ts.map +1 -0
  6. package/dist/adapters/agent-docs.js +156 -0
  7. package/dist/adapters/agent-docs.js.map +1 -0
  8. package/dist/adapters/agents-md.d.ts +15 -1
  9. package/dist/adapters/agents-md.d.ts.map +1 -1
  10. package/dist/adapters/agents-md.js +146 -120
  11. package/dist/adapters/agents-md.js.map +1 -1
  12. package/dist/adapters/e2e.d.ts +15 -0
  13. package/dist/adapters/e2e.d.ts.map +1 -0
  14. package/dist/adapters/e2e.js +55 -0
  15. package/dist/adapters/e2e.js.map +1 -0
  16. package/dist/adapters/skills.d.ts +5 -0
  17. package/dist/adapters/skills.d.ts.map +1 -1
  18. package/dist/adapters/skills.js +17 -0
  19. package/dist/adapters/skills.js.map +1 -1
  20. package/dist/cli/capabilities.d.ts +17 -0
  21. package/dist/cli/capabilities.d.ts.map +1 -0
  22. package/dist/cli/capabilities.js +149 -0
  23. package/dist/cli/capabilities.js.map +1 -0
  24. package/dist/cli/doctor.d.ts +16 -0
  25. package/dist/cli/doctor.d.ts.map +1 -1
  26. package/dist/cli/doctor.js +90 -70
  27. package/dist/cli/doctor.js.map +1 -1
  28. package/dist/cli/init.js +1 -1
  29. package/dist/cli/init.js.map +1 -1
  30. package/dist/cli/main.d.ts.map +1 -1
  31. package/dist/cli/main.js +117 -20
  32. package/dist/cli/main.js.map +1 -1
  33. package/dist/cli/new.d.ts +15 -1
  34. package/dist/cli/new.d.ts.map +1 -1
  35. package/dist/cli/new.js +157 -68
  36. package/dist/cli/new.js.map +1 -1
  37. package/dist/cli/wizard.d.ts +5 -0
  38. package/dist/cli/wizard.d.ts.map +1 -1
  39. package/dist/cli/wizard.js +104 -102
  40. package/dist/cli/wizard.js.map +1 -1
  41. package/dist/config/defaults.d.ts +14 -6
  42. package/dist/config/defaults.d.ts.map +1 -1
  43. package/dist/config/defaults.js +38 -174
  44. package/dist/config/defaults.js.map +1 -1
  45. package/dist/config/schema.d.ts +14 -0
  46. package/dist/config/schema.d.ts.map +1 -1
  47. package/dist/config/schema.js.map +1 -1
  48. package/dist/config/validation.d.ts +1 -0
  49. package/dist/config/validation.d.ts.map +1 -1
  50. package/dist/config/validation.js +63 -0
  51. package/dist/config/validation.js.map +1 -1
  52. package/dist/detect/index.d.ts +18 -0
  53. package/dist/detect/index.d.ts.map +1 -0
  54. package/dist/detect/index.js +120 -0
  55. package/dist/detect/index.js.map +1 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +5 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/presets/cli.d.ts.map +1 -1
  61. package/dist/presets/cli.js +7 -0
  62. package/dist/presets/cli.js.map +1 -1
  63. package/dist/presets/generic.d.ts.map +1 -1
  64. package/dist/presets/generic.js +4 -1
  65. package/dist/presets/generic.js.map +1 -1
  66. package/dist/presets/testing.d.ts +1 -1
  67. package/dist/presets/testing.d.ts.map +1 -1
  68. package/dist/presets/testing.js +22 -8
  69. package/dist/presets/testing.js.map +1 -1
  70. package/dist/presets/web.d.ts.map +1 -1
  71. package/dist/presets/web.js +5 -2
  72. package/dist/presets/web.js.map +1 -1
  73. package/dist/registry/catalog.d.ts +13 -0
  74. package/dist/registry/catalog.d.ts.map +1 -0
  75. package/dist/registry/catalog.js +544 -0
  76. package/dist/registry/catalog.js.map +1 -0
  77. package/dist/registry/index.d.ts +84 -0
  78. package/dist/registry/index.d.ts.map +1 -0
  79. package/dist/registry/index.js +309 -0
  80. package/dist/registry/index.js.map +1 -0
  81. package/dist/registry/setup.d.ts +14 -0
  82. package/dist/registry/setup.d.ts.map +1 -0
  83. package/dist/registry/setup.js +25 -0
  84. package/dist/registry/setup.js.map +1 -0
  85. package/dist/registry/types.d.ts +115 -0
  86. package/dist/registry/types.d.ts.map +1 -0
  87. package/dist/registry/types.js +2 -0
  88. package/dist/registry/types.js.map +1 -0
  89. package/package.json +3 -2
  90. package/src/adapters/agent-docs.ts +197 -0
  91. package/src/adapters/agents-md.ts +171 -137
  92. package/src/adapters/e2e.ts +58 -0
  93. package/src/adapters/skills.ts +18 -0
  94. package/src/cli/capabilities.ts +176 -0
  95. package/src/cli/doctor.ts +83 -66
  96. package/src/cli/init.ts +1 -1
  97. package/src/cli/main.ts +125 -20
  98. package/src/cli/new.ts +195 -74
  99. package/src/cli/wizard.ts +130 -103
  100. package/src/config/defaults.ts +57 -179
  101. package/src/config/schema.ts +15 -0
  102. package/src/config/validation.ts +56 -0
  103. package/src/detect/index.ts +134 -0
  104. package/src/index.ts +5 -0
  105. package/src/presets/cli.ts +6 -0
  106. package/src/presets/generic.ts +4 -1
  107. package/src/presets/testing.ts +29 -12
  108. package/src/presets/web.ts +4 -2
  109. package/src/registry/catalog.ts +569 -0
  110. package/src/registry/index.ts +395 -0
  111. package/src/registry/setup.ts +40 -0
  112. 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 { getPackageManagerAdapter } from './package-manager.js';
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
- export function generateAgentsMarkdown(
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
- scripts?: Record<string, string>
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 specSkillNames = ['to-spec', 'to-tickets', 'implement-spec'];
48
- let engineeringCapability = '';
49
- if (mattSkills.includes('*')) {
50
- engineeringCapability =
51
- '- **Engineering**: the full Matt Pocock skill set installed in `.agents/skills/` — start with `ask-matt` to route to the right one.';
52
- } else if (mattSkills.length) {
53
- const engineering = mattSkills.filter((skill) => !specSkillNames.includes(skill));
54
- if (engineering.length) {
55
- engineeringCapability = `- **Engineering**: ${engineering.map((skill) => '`' + skill + '`').join(', ')}`;
56
- }
57
- }
58
-
59
- let proseCapability = '';
60
- if (skillInstalled('humanizer')) {
61
- proseCapability =
62
- '- **Prose & Copy**: Use `humanizer` skill to eliminate AI writing tells and refine documentation.';
63
- }
64
-
65
- let simplicityCapability = '';
66
- if (skillInstalled('ponytail')) {
67
- simplicityCapability =
68
- '- **Simplicity**: Use `ponytail` skill for the smallest complete change and to cut over-engineering.';
69
- }
70
-
71
- let uiCapability = '';
72
- if (isWeb && skillInstalled('impeccable')) {
73
- uiCapability =
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
- const sections: Array<string | undefined> = [
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
- `- Package Manager: \`${pm}\``,
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
- '## AI Capabilities & Skills',
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
- '## Bootstrap & First Session',
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
- // Conditional lines are `undefined` and dropped; `''` entries are the blank
135
- // separators between sections and must survive.
136
- return (
137
- sections
138
- .filter((s): s is string => s !== undefined)
139
- .join('\n')
140
- .trim() + '\n'
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
- if (!existing.includes('## Bootstrap & First Session')) {
156
- const addition = [
157
- '',
158
- '## Bootstrap & First Session',
159
- 'When the user asks to "bootstrap this project" or kicks off a new initiative:',
160
- '1. Understand product intent and ask 2-3 essential questions to resolve ambiguity.',
161
- '2. Draft or refine canonical context in `PRODUCT.md` and `DESIGN.md` (or `docs/adr/`).',
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('AGENTS.md already exists, preserving content.');
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
+ }
@@ -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
+ }