@antomanc/aginit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +108 -28
  3. package/THIRD_PARTY_NOTICES.md +92 -16
  4. package/dist/adapters/agent-docs.d.ts +32 -0
  5. package/dist/adapters/agent-docs.d.ts.map +1 -0
  6. package/dist/adapters/agent-docs.js +146 -0
  7. package/dist/adapters/agent-docs.js.map +1 -0
  8. package/dist/adapters/agents-md.d.ts +13 -1
  9. package/dist/adapters/agents-md.d.ts.map +1 -1
  10. package/dist/adapters/agents-md.js +137 -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/adapters/tool-ignores.d.ts +9 -0
  21. package/dist/adapters/tool-ignores.d.ts.map +1 -0
  22. package/dist/adapters/tool-ignores.js +121 -0
  23. package/dist/adapters/tool-ignores.js.map +1 -0
  24. package/dist/cli/capabilities.d.ts +17 -0
  25. package/dist/cli/capabilities.d.ts.map +1 -0
  26. package/dist/cli/capabilities.js +149 -0
  27. package/dist/cli/capabilities.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +16 -0
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +104 -70
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/init.js +1 -1
  33. package/dist/cli/init.js.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/main.js +123 -20
  36. package/dist/cli/main.js.map +1 -1
  37. package/dist/cli/new.d.ts +15 -1
  38. package/dist/cli/new.d.ts.map +1 -1
  39. package/dist/cli/new.js +163 -67
  40. package/dist/cli/new.js.map +1 -1
  41. package/dist/cli/wizard.d.ts +5 -0
  42. package/dist/cli/wizard.d.ts.map +1 -1
  43. package/dist/cli/wizard.js +104 -102
  44. package/dist/cli/wizard.js.map +1 -1
  45. package/dist/config/defaults.d.ts +14 -6
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +38 -174
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/schema.d.ts +14 -0
  50. package/dist/config/schema.d.ts.map +1 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/validation.d.ts +1 -0
  53. package/dist/config/validation.d.ts.map +1 -1
  54. package/dist/config/validation.js +63 -0
  55. package/dist/config/validation.js.map +1 -1
  56. package/dist/detect/index.d.ts +18 -0
  57. package/dist/detect/index.d.ts.map +1 -0
  58. package/dist/detect/index.js +120 -0
  59. package/dist/detect/index.js.map +1 -0
  60. package/dist/index.d.ts +5 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +5 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/presets/cli.d.ts.map +1 -1
  65. package/dist/presets/cli.js +7 -0
  66. package/dist/presets/cli.js.map +1 -1
  67. package/dist/presets/generic.d.ts.map +1 -1
  68. package/dist/presets/generic.js +4 -1
  69. package/dist/presets/generic.js.map +1 -1
  70. package/dist/presets/testing.d.ts +1 -1
  71. package/dist/presets/testing.d.ts.map +1 -1
  72. package/dist/presets/testing.js +22 -8
  73. package/dist/presets/testing.js.map +1 -1
  74. package/dist/presets/web.d.ts.map +1 -1
  75. package/dist/presets/web.js +39 -5
  76. package/dist/presets/web.js.map +1 -1
  77. package/dist/registry/catalog.d.ts +13 -0
  78. package/dist/registry/catalog.d.ts.map +1 -0
  79. package/dist/registry/catalog.js +544 -0
  80. package/dist/registry/catalog.js.map +1 -0
  81. package/dist/registry/index.d.ts +84 -0
  82. package/dist/registry/index.d.ts.map +1 -0
  83. package/dist/registry/index.js +309 -0
  84. package/dist/registry/index.js.map +1 -0
  85. package/dist/registry/setup.d.ts +14 -0
  86. package/dist/registry/setup.d.ts.map +1 -0
  87. package/dist/registry/setup.js +25 -0
  88. package/dist/registry/setup.js.map +1 -0
  89. package/dist/registry/types.d.ts +115 -0
  90. package/dist/registry/types.d.ts.map +1 -0
  91. package/dist/registry/types.js +2 -0
  92. package/dist/registry/types.js.map +1 -0
  93. package/package.json +3 -2
  94. package/src/adapters/agent-docs.ts +184 -0
  95. package/src/adapters/agents-md.ts +160 -137
  96. package/src/adapters/e2e.ts +58 -0
  97. package/src/adapters/skills.ts +18 -0
  98. package/src/adapters/tool-ignores.ts +158 -0
  99. package/src/cli/capabilities.ts +176 -0
  100. package/src/cli/doctor.ts +100 -66
  101. package/src/cli/init.ts +1 -1
  102. package/src/cli/main.ts +130 -20
  103. package/src/cli/new.ts +199 -73
  104. package/src/cli/wizard.ts +130 -103
  105. package/src/config/defaults.ts +57 -179
  106. package/src/config/schema.ts +15 -0
  107. package/src/config/validation.ts +56 -0
  108. package/src/detect/index.ts +134 -0
  109. package/src/index.ts +5 -0
  110. package/src/presets/cli.ts +6 -0
  111. package/src/presets/generic.ts +4 -1
  112. package/src/presets/testing.ts +29 -12
  113. package/src/presets/web.ts +39 -5
  114. package/src/registry/catalog.ts +569 -0
  115. package/src/registry/index.ts +395 -0
  116. package/src/registry/setup.ts +40 -0
  117. package/src/registry/types.ts +109 -0
@@ -0,0 +1,184 @@
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
+ '<!-- Generated by aginit from aginit.config.json. Rerun `aginit` to refresh; delete this line to edit by hand. -->';
60
+
61
+ const DOC_TITLES: Record<MicroDoc, string> = {
62
+ testing: 'Testing',
63
+ 'ui-verification': 'Live UI verification',
64
+ workflows: 'Engineering workflows & skills'
65
+ };
66
+
67
+ const skillNames = (capability: CapabilityDescriptor) =>
68
+ capability.install.flatMap((step) => (step.kind === 'skills-sh' ? step.skills : []));
69
+
70
+ function testingIntro(profile: ProjectProfile): string[] {
71
+ const { commands } = profile;
72
+ const rows: Array<[string, string | undefined]> = [
73
+ ['Build', commands.build],
74
+ ['Typecheck', commands.typecheck],
75
+ ['Lint / analyze', commands.lint],
76
+ ['Test', commands.test],
77
+ ['Unit test', commands.unitTest],
78
+ ['E2E', commands.e2e]
79
+ ];
80
+ const lines = rows.filter(([, cmd]) => cmd).map(([label, cmd]) => `- ${label}: \`${cmd}\``);
81
+ return lines.length
82
+ ? ['Run the full check before calling a change done:', ...lines]
83
+ : ['Record the project build and test commands here once they exist.'];
84
+ }
85
+
86
+ function workflowsExtras(profile: ProjectProfile): string[] {
87
+ // Skill capabilities without their own guide still show up, so new registry
88
+ // entries and custom skill repos are documented without extra code.
89
+ const plain = profile.active.filter(
90
+ (c) => !c.guide && c.install.some((step) => step.kind === 'skills-sh')
91
+ );
92
+ const bullet = (c: CapabilityDescriptor) => {
93
+ const skills = skillNames(c);
94
+ const label = skills.length ? skills.map((s) => '`' + s + '`').join(', ') : `\`${c.id}\``;
95
+ return `- ${label}: ${c.description}.`;
96
+ };
97
+ const discipline = plain.filter((c) => c.category === 'discipline');
98
+ const others = plain.filter((c) => c.category !== 'discipline');
99
+ return [
100
+ ...(discipline.length ? ['## Discipline (apply to every change)', ...discipline.map(bullet)] : []),
101
+ ...(others.length ? ['', '## Other skills', ...others.map(bullet)] : [])
102
+ ];
103
+ }
104
+
105
+ // Drivers come before the design review that uses them; unit runners before E2E.
106
+ const SECTION_ORDER = ['methodology', 'ui-driver', 'design', 'testing-engine', 'context-guard'];
107
+ const sectionRank = (c: CapabilityDescriptor) => {
108
+ const category = SECTION_ORDER.indexOf(c.category);
109
+ const index = CATALOG.indexOf(c);
110
+ return (category === -1 ? SECTION_ORDER.length : category) * 1000 + (index === -1 ? 999 : index);
111
+ };
112
+
113
+ /** Markdown for one micro-doc, or null when no active capability contributes to it. */
114
+ export function renderMicroDoc(doc: MicroDoc, profile: ProjectProfile): string | null {
115
+ const sections = profile.active
116
+ .filter((c) => c.guide?.doc === doc)
117
+ .sort((a, b) => sectionRank(a) - sectionRank(b))
118
+ .map((c) => c.guide!.render(profile));
119
+ const extras = doc === 'workflows' ? workflowsExtras(profile) : [];
120
+ if (sections.length === 0 && extras.length === 0) return null;
121
+ const intro =
122
+ doc === 'testing'
123
+ ? testingIntro(profile)
124
+ : doc === 'workflows'
125
+ ? [
126
+ 'Skills live in `.agents/skills/` (linked into `.claude/skills/` for Claude Code). Invoke one by name',
127
+ '(for example `/tdd`), or let the agent pick it from its description.'
128
+ ]
129
+ : ['Verify UI changes in the running app, not only in tests.'];
130
+ const body = [`# ${DOC_TITLES[doc]}`, '', ...intro, '', ...sections.flatMap((s) => [s, ''])];
131
+ if (extras.length) body.push(...extras, '');
132
+ return [GENERATED_MARKER, ...body].join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
133
+ }
134
+
135
+ export const MICRO_DOCS: MicroDoc[] = ['testing', 'ui-verification', 'workflows'];
136
+
137
+ export function microDocPath(doc: MicroDoc): string {
138
+ return path.join(DOCS_DIR, `${doc}.md`).split(path.sep).join('/');
139
+ }
140
+
141
+ /** Docs aginit owns: files it wrote that still carry the generated marker. */
142
+ function ownedByAginit(file: string): boolean {
143
+ const text = readTextFile(file);
144
+ return text === null || text.startsWith(GENERATED_MARKER);
145
+ }
146
+
147
+ /**
148
+ * Write the docs/agents/ micro-docs for the active capabilities. A doc whose
149
+ * marker line was removed belongs to the user and is never touched; a doc no
150
+ * capability contributes to any more is deleted. Returns the docs that exist.
151
+ */
152
+ export function setupMicroDocs(
153
+ targetDir: string,
154
+ profile: ProjectProfile,
155
+ options: { dryRun?: boolean; silent?: boolean } = {}
156
+ ): MicroDoc[] {
157
+ const { dryRun = false, silent = false } = options;
158
+ const present: MicroDoc[] = [];
159
+ for (const doc of MICRO_DOCS) {
160
+ const file = path.join(targetDir, DOCS_DIR, `${doc}.md`);
161
+ const content = renderMicroDoc(doc, profile);
162
+ if (!ownedByAginit(file)) {
163
+ if (!silent) logger.dim(`${microDocPath(doc)} was edited by hand, preserving it.`);
164
+ present.push(doc);
165
+ continue;
166
+ }
167
+ if (content === null) {
168
+ if (fs.existsSync(file)) {
169
+ if (dryRun) logger.dim(`[dry-run] Would remove file: ${file}`);
170
+ else fs.rmSync(file);
171
+ }
172
+ continue;
173
+ }
174
+ present.push(doc);
175
+ if (readTextFile(file) !== content)
176
+ safeWriteFile(file, content, { overwrite: true, dryRun, silent });
177
+ }
178
+ return present;
179
+ }
180
+
181
+ /** Docs that will exist for a profile (used when rendering AGENTS.md without a target). */
182
+ export function plannedMicroDocs(profile: ProjectProfile): MicroDoc[] {
183
+ return MICRO_DOCS.filter((doc) => renderMicroDoc(doc, profile) !== null);
184
+ }
@@ -3,97 +3,147 @@ 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 = '<!-- aginit:start -->';
24
+ export const AGINIT_BLOCK_END = '<!-- aginit:end -->';
25
+
26
+ const BOOTSTRAP_SECTION = [
27
+ '## Bootstrap & First Session',
28
+ 'When the user asks to "bootstrap this project" or kicks off a new initiative:',
29
+ '1. Understand product intent and ask 2-3 essential questions to resolve ambiguity.',
30
+ '2. Draft or refine canonical context in `PRODUCT.md` and `DESIGN.md` (or `docs/adr/`).',
31
+ '3. Plan in small verifiable increments and implement test-first using installed skills.'
32
+ ];
33
+
34
+ /** Placeholder scripts used when AGENTS.md is rendered without a package.json to read. */
35
+ function presetScripts(config: AginitConfig): Record<string, string> {
36
+ return config.preset === 'generic'
37
+ ? { test: 'vitest run' }
38
+ : { build: '', typecheck: '', test: '', dev: '', 'test:e2e': '' };
39
+ }
7
40
 
8
- export function generateAgentsMarkdown(
41
+ const table = (head: [string, string], rows: Array<[string, string]>) => [
42
+ `| ${head[0]} | ${head[1]} |`,
43
+ '| --- | --- |',
44
+ ...rows.map(([a, b]) => `| ${a} | ${b} |`)
45
+ ];
46
+
47
+ /**
48
+ * The part of AGENTS.md aginit keeps in sync with the active capabilities:
49
+ * guardrails, the Task Routing table and the Anti-Rationalization table.
50
+ */
51
+ export function renderAgentsBlock(
9
52
  config: AginitConfig,
10
- scripts?: Record<string, string>
53
+ profile: ProjectProfile,
54
+ docs: MicroDoc[] = plannedMicroDocs(profile)
11
55
  ): 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
56
  const hasSkills = config.skills.sources.length > 0;
57
+ const routing: Array<[string, string]> = [
58
+ ['Product intent, scope, target users', '`PRODUCT.md`']
59
+ ];
60
+ const order = ['design', 'codebase-graph'];
61
+ const rank = (c: CapabilityDescriptor) => (order.indexOf(c.category) + order.length + 1) % (order.length + 1);
62
+ const direct = profile.active.filter((c) => c.routing).sort((a, b) => rank(a) - rank(b));
63
+ const routeOf = (c: CapabilityDescriptor): [string, string] => [c.routing!.task, c.routing!.read];
64
+ // Design and codebase rows lead, discipline rows follow the micro-docs.
65
+ routing.push(...direct.filter((c) => c.category !== 'discipline').map(routeOf));
66
+ if (docs.includes('testing'))
67
+ routing.push(['Automated unit & E2E testing', '`' + microDocPath('testing') + '`']);
68
+ if (docs.includes('ui-verification'))
69
+ routing.push(['Live UI verification (Web / Mobile)', '`' + microDocPath('ui-verification') + '`']);
70
+ if (docs.includes('workflows'))
71
+ routing.push(['Engineering workflow & skills', '`' + microDocPath('workflows') + '`']);
72
+ routing.push(...direct.filter((c) => c.category === 'discipline').map(routeOf));
73
+
74
+ const rebuttals = new Map<string, string>();
75
+ for (const capability of profile.active)
76
+ for (const { excuse, rule } of capability.rebuttals ?? [])
77
+ if (!rebuttals.has(excuse)) rebuttals.set(excuse, rule);
78
+ const unitExcuse = 'Unit tests pass, so the feature works.';
79
+ if (!rebuttals.has(unitExcuse) && profile.active.some((c) => c.category === 'testing-engine'))
80
+ rebuttals.set(
81
+ unitExcuse,
82
+ 'Insufficient. Unit tests do not verify wiring or rendering. Exercise the feature end to end ' +
83
+ `(${profile.commands.e2e && profile.has('playwright') ? '`' + profile.commands.e2e + '`' : 'integration or smoke run'}) before marking complete.`
84
+ );
46
85
 
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
- : '';
86
+ const lines = [
87
+ AGINIT_BLOCK_START,
88
+ ...(profile.has('karpathy-guidelines')
89
+ ? [
90
+ '## Guardrails (karpathy-guidelines)',
91
+ '- **Think before coding**: state assumptions and trade-offs; ask instead of guessing.',
92
+ '- **Simplicity first**: minimum code that solves the problem; no speculative features or abstractions.',
93
+ '- **Surgical changes**: touch only what the task needs; no silent scope creep or drive-by refactors.',
94
+ '- **Goal-driven execution**: define a verifiable success check first, then loop until it passes.',
95
+ ''
96
+ ]
97
+ : []),
98
+ '## Task Routing',
99
+ hasSkills
100
+ ? config.agents.claudeCode === false
101
+ ? 'Load only what the task needs. Skills live in `.agents/skills/`.'
102
+ : 'Load only what the task needs. Skills live in `.agents/skills/` (linked into `.claude/skills/` for Claude Code).'
103
+ : 'Load only what the task needs. Skills installation is disabled in this configuration.',
104
+ '',
105
+ ...table(['Task', 'Read / Use'], routing),
106
+ ...(rebuttals.size
107
+ ? ['', '## Anti-Rationalization', ...table(['Excuse', 'Rule'], [...rebuttals].map(([e, r]) => [`"${e}"`, r]))]
108
+ : []),
109
+ AGINIT_BLOCK_END
110
+ ];
111
+ return lines.join('\n');
112
+ }
92
113
 
93
- const sections: Array<string | undefined> = [
114
+ export function generateAgentsMarkdown(
115
+ config: AginitConfig,
116
+ scripts?: Record<string, string>,
117
+ targetDir?: string
118
+ ): string {
119
+ const resolved = resolvePlatformStack(config, targetDir);
120
+ const node = isNodeStack(resolved.stack);
121
+ const profile = projectProfile(config, {
122
+ targetDir,
123
+ scripts: node ? (scripts ?? presetScripts(config)) : undefined
124
+ });
125
+ const { commands } = profile;
126
+ const pm = config.packageManager || 'pnpm';
127
+ const isWeb = profile.platform === 'web';
128
+ const commandLines = [
129
+ node ? `- Package Manager: \`${pm}\`` : undefined,
130
+ commands.dev && isWeb && config.framework && config.framework !== 'none'
131
+ ? `- Dev Server: \`${commands.dev}\``
132
+ : undefined,
133
+ commands.build ? `- Build: \`${commands.build}\`` : undefined,
134
+ commands.typecheck ? `- Typecheck: \`${commands.typecheck}\`` : undefined,
135
+ commands.lint ? `- Lint: \`${commands.lint}\`` : undefined,
136
+ commands.test ? `- Test: \`${commands.test}\`` : undefined,
137
+ commands.unitTest ? `- Unit Test: \`${commands.unitTest}\`` : undefined,
138
+ commands.e2e && profile.has('playwright')
139
+ ? `- Test (E2E / Browser): \`${commands.e2e}\``
140
+ : undefined
141
+ ].filter((line): line is string => line !== undefined);
142
+
143
+ const sections: string[] = [
94
144
  `# ${config.name}`,
95
145
  '',
96
- `AI-first ${config.preset} project configured with \`aginit\`.`,
146
+ `AI-first ${config.preset} project configured with \`aginit\` (${PLATFORM_LABELS[profile.platform]}, ${profile.stack}).`,
97
147
  '',
98
148
  '## Core Invariants',
99
149
  '- Keep changes small, focused, and test-verified.',
@@ -101,80 +151,53 @@ export function generateAgentsMarkdown(
101
151
  '- Never commit broken builds, failed typechecks, or unverified changes.',
102
152
  '',
103
153
  '## 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,
154
+ ...(commandLines.length ? commandLines : ['- Add the build and test commands here once they exist.']),
111
155
  '',
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,
156
+ renderAgentsBlock(config, profile),
125
157
  '',
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.',
158
+ ...BOOTSTRAP_SECTION,
131
159
  ''
132
160
  ];
161
+ return sections.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
162
+ }
133
163
 
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
- );
164
+ /** Insert or refresh the aginit block in an existing AGENTS.md, leaving everything else as written. */
165
+ export function mergeAgentsBlock(existing: string, block: string): string {
166
+ const start = existing.indexOf(AGINIT_BLOCK_START);
167
+ const end = existing.indexOf(AGINIT_BLOCK_END);
168
+ if (start !== -1 && end > start)
169
+ return existing.slice(0, start) + block + existing.slice(end + AGINIT_BLOCK_END.length);
170
+ // Above Graft's block when there is one, so Graft keeps owning the end of the file.
171
+ const graft = existing.indexOf('<!-- graft:start -->');
172
+ if (graft !== -1)
173
+ return existing.slice(0, graft).trimEnd() + '\n\n' + block + '\n\n' + existing.slice(graft);
174
+ return existing.trimEnd() + '\n\n' + block + '\n';
142
175
  }
143
176
 
144
177
  export function setupAgentsMarkdown(
145
178
  targetDir: string,
146
179
  config: AginitConfig,
147
- options: { overwrite?: boolean; dryRun?: boolean; silent?: boolean } = {}
180
+ options: { overwrite?: boolean; dryRun?: boolean; silent?: boolean; docs?: MicroDoc[] } = {}
148
181
  ): boolean {
149
182
  const { overwrite = false, dryRun = false, silent = false } = options;
150
183
  const filePath = path.join(targetDir, 'AGENTS.md');
151
184
 
152
185
  if (fileExists(filePath) && !overwrite) {
153
- // File exists; check if it has the bootstrap guidance. If not, append it conservatively.
154
186
  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');
187
+ const scripts = readPackageManifest(targetDir)?.scripts;
188
+ const profile = projectProfile(config, { targetDir, scripts });
189
+ let updated = mergeAgentsBlock(existing, renderAgentsBlock(config, profile, options.docs));
190
+ if (!updated.includes('## Bootstrap & First Session'))
191
+ updated = updated.trimEnd() + '\n\n' + BOOTSTRAP_SECTION.join('\n') + '\n';
192
+ if (updated === existing) {
193
+ if (!silent) logger.dim('AGENTS.md is up to date, preserving content.');
171
194
  return true;
172
195
  }
173
- if (!silent) logger.dim('AGENTS.md already exists, preserving content.');
174
- return true;
196
+ if (!silent) logger.dim('Refreshed the aginit section of the existing AGENTS.md');
197
+ return safeWriteFile(filePath, updated, { overwrite: true, dryRun, silent });
175
198
  }
176
199
 
177
- const content = generateAgentsMarkdown(config, readPackageManifest(targetDir)?.scripts);
200
+ const content = generateAgentsMarkdown(config, readPackageManifest(targetDir)?.scripts, targetDir);
178
201
  return safeWriteFile(filePath, content, { overwrite: true, dryRun, silent });
179
202
  }
180
203
 
@@ -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
+ }