@antomanc/aginit 0.5.0 → 0.6.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 (75) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +21 -16
  3. package/THIRD_PARTY_NOTICES.md +3 -3
  4. package/dist/adapters/agent-docs.d.ts +4 -1
  5. package/dist/adapters/agent-docs.d.ts.map +1 -1
  6. package/dist/adapters/agent-docs.js +17 -5
  7. package/dist/adapters/agent-docs.js.map +1 -1
  8. package/dist/adapters/agents-md.d.ts +7 -2
  9. package/dist/adapters/agents-md.d.ts.map +1 -1
  10. package/dist/adapters/agents-md.js +33 -20
  11. package/dist/adapters/agents-md.js.map +1 -1
  12. package/dist/adapters/git.d.ts.map +1 -1
  13. package/dist/adapters/git.js +0 -6
  14. package/dist/adapters/git.js.map +1 -1
  15. package/dist/adapters/tool-ignores.d.ts.map +1 -1
  16. package/dist/adapters/tool-ignores.js +25 -1
  17. package/dist/adapters/tool-ignores.js.map +1 -1
  18. package/dist/adapters/trail.d.ts +15 -0
  19. package/dist/adapters/{graft.d.ts.map → trail.d.ts.map} +1 -1
  20. package/dist/adapters/trail.js +68 -0
  21. package/dist/adapters/trail.js.map +1 -0
  22. package/dist/cli/capabilities.js +8 -8
  23. package/dist/cli/doctor.js +8 -8
  24. package/dist/cli/doctor.js.map +1 -1
  25. package/dist/cli/main.js +13 -13
  26. package/dist/cli/new.d.ts +1 -1
  27. package/dist/cli/new.js +5 -5
  28. package/dist/cli/wizard.d.ts +1 -0
  29. package/dist/cli/wizard.d.ts.map +1 -1
  30. package/dist/cli/wizard.js +5 -3
  31. package/dist/cli/wizard.js.map +1 -1
  32. package/dist/config/defaults.js +1 -1
  33. package/dist/config/schema.d.ts +2 -2
  34. package/dist/config/validation.d.ts +1 -1
  35. package/dist/config/validation.d.ts.map +1 -1
  36. package/dist/config/validation.js +15 -3
  37. package/dist/config/validation.js.map +1 -1
  38. package/dist/detect/index.d.ts +2 -0
  39. package/dist/detect/index.d.ts.map +1 -1
  40. package/dist/detect/index.js +15 -2
  41. package/dist/detect/index.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/registry/catalog.d.ts.map +1 -1
  45. package/dist/registry/catalog.js +14 -10
  46. package/dist/registry/catalog.js.map +1 -1
  47. package/dist/registry/index.d.ts +1 -1
  48. package/dist/registry/index.js +5 -5
  49. package/dist/registry/setup.js +3 -3
  50. package/dist/registry/types.d.ts +3 -1
  51. package/dist/registry/types.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/adapters/agent-docs.ts +19 -4
  54. package/src/adapters/agents-md.ts +38 -20
  55. package/src/adapters/git.ts +0 -6
  56. package/src/adapters/tool-ignores.ts +27 -1
  57. package/src/adapters/trail.ts +94 -0
  58. package/src/cli/capabilities.ts +6 -6
  59. package/src/cli/doctor.ts +8 -8
  60. package/src/cli/main.ts +14 -14
  61. package/src/cli/new.ts +5 -5
  62. package/src/cli/wizard.ts +7 -3
  63. package/src/config/defaults.ts +1 -1
  64. package/src/config/schema.ts +2 -2
  65. package/src/config/validation.ts +17 -3
  66. package/src/detect/index.ts +15 -2
  67. package/src/index.ts +1 -1
  68. package/src/registry/catalog.ts +15 -10
  69. package/src/registry/index.ts +4 -4
  70. package/src/registry/setup.ts +4 -4
  71. package/src/registry/types.ts +4 -2
  72. package/dist/adapters/graft.d.ts +0 -15
  73. package/dist/adapters/graft.js +0 -58
  74. package/dist/adapters/graft.js.map +0 -1
  75. package/src/adapters/graft.ts +0 -82
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileExists, readTextFile, safeWriteFile } from '../utils/fs.js';
4
+ import { commandExists, runExecutable } from '../utils/shell.js';
5
+ import { logger } from '../utils/logger.js';
6
+
7
+ // The ripgrep hint graft wrote at the repo root; trail keeps its graph in .trail/cache.
8
+ const GRAFT_RIPGREP_HINT =
9
+ "# graft's cards are gitignored but should stay greppable: ripgrep reads\n" +
10
+ '# .ignore before .gitignore, so this re-admits the tree to search only.\n' +
11
+ '!graft/\ngraft/.cache/\ngraft/.graph/\n';
12
+
13
+ export async function isTrailInstalled(): Promise<boolean> {
14
+ return await commandExists('trail');
15
+ }
16
+
17
+ export async function initTrail(
18
+ targetDir: string,
19
+ options: { dryRun?: boolean; silent?: boolean; claude?: boolean } = {}
20
+ ): Promise<boolean> {
21
+ const { dryRun = false, silent = false, claude = true } = options;
22
+
23
+ const installed = dryRun || (await isTrailInstalled());
24
+ if (!installed) {
25
+ if (!silent) {
26
+ logger.warn(
27
+ 'Trail CLI is not installed. Install it with `npm install -g @trailhq/trail-code`, then rerun aginit init.'
28
+ );
29
+ }
30
+ return false;
31
+ }
32
+
33
+ if (!silent) logger.dim('Initializing Trail context graph...');
34
+
35
+ // Wire Antigravity, AGENTS.md readers and (optionally) Claude Code's in-repo
36
+ // .claude/ + .trail/, without touching global system files (--no-global).
37
+ // trail init also migrates a repo graft wired before.
38
+ const agents = ['antigravity', 'agents', ...(claude ? ['claude'] : [])];
39
+ const res = await runExecutable(
40
+ 'trail',
41
+ ['init', '--no-global', '--agents', ...agents, '-y'],
42
+ {
43
+ cwd: targetDir,
44
+ dryRun,
45
+ silent: true
46
+ }
47
+ );
48
+
49
+ if (!res.ok) {
50
+ logger.warn(`Trail initialization warning: ${res.stderr || res.stdout}`);
51
+ return false;
52
+ }
53
+
54
+ // trail init leaves graft's ripgrep hint behind; drop it when unedited.
55
+ const hint = path.join(targetDir, '.ignore');
56
+ if (!dryRun && readTextFile(hint) === GRAFT_RIPGREP_HINT) fs.rmSync(hint);
57
+
58
+ return true;
59
+ }
60
+
61
+ export async function buildTrail(
62
+ targetDir: string,
63
+ options: { dryRun?: boolean; silent?: boolean } = {}
64
+ ): Promise<boolean> {
65
+ const { dryRun = false } = options;
66
+ const installed = dryRun || (await isTrailInstalled());
67
+ if (!installed) return false;
68
+
69
+ const res = await runExecutable('trail', ['build'], { cwd: targetDir, dryRun, silent: true });
70
+ return res.ok;
71
+ }
72
+
73
+ export async function uninstallTrail(
74
+ targetDir: string,
75
+ options: { dryRun?: boolean; silent?: boolean } = {}
76
+ ): Promise<boolean> {
77
+ const { dryRun = false } = options;
78
+ const installed = dryRun || (await isTrailInstalled());
79
+ if (installed) {
80
+ await runExecutable('trail', ['uninstall'], { cwd: targetDir, dryRun, silent: true });
81
+ }
82
+
83
+ // Also clean up AGENTS.md fenced section if any remain (graft's included)
84
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
85
+ if (fileExists(agentsPath)) {
86
+ const content = readTextFile(agentsPath) || '';
87
+ const cleaned = content
88
+ .replace(/<!-- (trail|graft):start -->[\s\S]*?<!-- \1:end -->\n?/g, '')
89
+ .trim();
90
+ safeWriteFile(agentsPath, cleaned + '\n', { overwrite: true, dryRun });
91
+ }
92
+
93
+ return true;
94
+ }
@@ -4,7 +4,7 @@ import { AGINIT_CONFIG_FILENAME, AginitConfig } from '../config/schema.js';
4
4
  import { readProjectConfig, validateConfig } from '../config/validation.js';
5
5
  import { resolvePlatformStack } from '../adapters/agent-docs.js';
6
6
  import { installAllSkills, removeSkills, skillAgents } from '../adapters/skills.js';
7
- import { uninstallGraft } from '../adapters/graft.js';
7
+ import { uninstallTrail } from '../adapters/trail.js';
8
8
  import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
9
9
  import {
10
10
  activeIds,
@@ -121,8 +121,8 @@ export async function changeCapabilities(
121
121
  const next = validateConfig(
122
122
  applyResolvedProfile({ ...config, platform, stack }, resolved, { skills: true, previous })
123
123
  );
124
- // Adding graft or playwright turns their legacy switch back on.
125
- if (next.capabilities?.includes('graft')) next.codebase.graft = true;
124
+ // Adding trail or playwright turns their legacy switch back on.
125
+ if (next.capabilities?.includes('trail')) next.codebase.trail = true;
126
126
 
127
127
  const after = activeIds(next);
128
128
  const nextCatalog = catalogFor(next);
@@ -141,9 +141,9 @@ export async function changeCapabilities(
141
141
  let ok = true;
142
142
 
143
143
  ok = (await removeSkills(targetDir, skillsOf(removed), { dryRun, silent })) && ok;
144
- if (removed.some((c) => c.id === 'graft')) await uninstallGraft(targetDir, { dryRun, silent });
144
+ if (removed.some((c) => c.id === 'trail')) await uninstallTrail(targetDir, { dryRun, silent });
145
145
  const leftovers = removed.filter((c) =>
146
- c.install.some((s) => s.kind === 'setup' && s.setup !== 'graft')
146
+ c.install.some((s) => s.kind === 'setup' && s.setup !== 'trail')
147
147
  );
148
148
  if (!silent)
149
149
  for (const c of leftovers)
@@ -166,7 +166,7 @@ export async function changeCapabilities(
166
166
 
167
167
  syncAgentDocs(targetDir, next, { dryRun, silent });
168
168
  if (!silent)
169
- for (const { name, bin, install } of await missingBinaries(added, ['graft', 'agent-browser', 'e2e']))
169
+ for (const { name, bin, install } of await missingBinaries(added, ['trail', 'agent-browser', 'e2e']))
170
170
  logger.warn(`${name} needs the \`${bin}\` CLI: ${install}`);
171
171
  if (!silent) {
172
172
  if (ok) logger.success('Capabilities updated. AGENTS.md and docs/agents/ are in sync.');
package/src/cli/doctor.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  getInstalledSkills,
16
16
  getSkillsMissingForClaude
17
17
  } from '../adapters/skills.js';
18
- import { claudeMarkdownImportsAgents } from '../adapters/agents-md.js';
18
+ import { AGINIT_START_PATTERN, claudeMarkdownImportsAgents } from '../adapters/agents-md.js';
19
19
  import {
20
20
  agentBrowserStubInstalled,
21
21
  findUsernsRestriction,
@@ -229,21 +229,21 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
229
229
  const agentsPath = path.join(targetDir, 'AGENTS.md');
230
230
  const agentsExists = fileExists(agentsPath);
231
231
  const agentsContent = agentsExists ? readTextFile(agentsPath) || '' : '';
232
- const hasIntakeSection = agentsContent.includes('Bootstrap & First Session');
232
+ const hasBlock = AGINIT_START_PATTERN.test(agentsContent);
233
233
 
234
234
  checks.push({
235
235
  name: 'AGENTS.md Context',
236
236
  category: 'Project Config',
237
- ok: agentsExists && hasIntakeSection,
237
+ ok: agentsExists && hasBlock,
238
238
  detail: agentsExists
239
- ? hasIntakeSection
240
- ? 'AGENTS.md present with intake/bootstrap guidance'
241
- : 'AGENTS.md present but missing intake guidance'
239
+ ? hasBlock
240
+ ? 'AGENTS.md present with the aginit block'
241
+ : 'AGENTS.md present but missing the aginit block'
242
242
  : 'AGENTS.md not found in directory',
243
243
  suggestion: !agentsExists
244
244
  ? 'Run `aginit init` to generate standard AGENTS.md'
245
- : !hasIntakeSection
246
- ? 'Run `aginit init` to add intake section to AGENTS.md'
245
+ : !hasBlock
246
+ ? 'Run `aginit init` to add the aginit block to AGENTS.md'
247
247
  : undefined
248
248
  });
249
249
 
package/src/cli/main.ts CHANGED
@@ -5,7 +5,7 @@ import { initCurrentDirectory } from './init.js';
5
5
  import { runDoctor } from './doctor.js';
6
6
  import { ensureClaudeSkillLinks, updateSkills } from '../adapters/skills.js';
7
7
  import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
8
- import { buildGraft } from '../adapters/graft.js';
8
+ import { buildTrail } from '../adapters/trail.js';
9
9
  import { readProjectConfig } from '../config/validation.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { PresetType, WebFramework, PackageManager } from '../config/schema.js';
@@ -50,7 +50,7 @@ function withProfileOptions(command: Command): Command {
50
50
  .option('--ui-driver <driver>', 'UI driver: auto | agent-browser | agent-device | none')
51
51
  .option('--addons <ids>', 'Add-ons: context-mode, worktrunk, chrome-devtools-mcp, browserskill', list)
52
52
  .option('--add-skill <owner/repo>', 'Add a skill repository as a custom capability (repeatable)', list)
53
- .option('--without <id>', 'Disable a capability, e.g. graft or humanizer (repeatable)', list);
53
+ .option('--without <id>', 'Disable a capability, e.g. trail or humanizer (repeatable)', list);
54
54
  }
55
55
 
56
56
  function profileFlags(options: any): ProfileFlags {
@@ -78,7 +78,7 @@ export function createCliProgram(): Command {
78
78
  .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing')
79
79
  .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun')
80
80
  .option('--no-skills', 'Skip installing AI skills')
81
- .option('--no-graft', 'Skip codebase graph (Graft) setup')
81
+ .option('--no-trail', 'Skip codebase graph (Trail) setup')
82
82
  .option('--no-git', 'Skip git repository initialization')
83
83
  .option('--dry-run', 'Simulate setup without writing changes to disk')
84
84
  .option('-y, --yes', 'Initialize the current directory without prompts (saved or detected settings)');
@@ -101,7 +101,7 @@ export function createCliProgram(): Command {
101
101
  framework: options.framework as WebFramework | undefined,
102
102
  packageManager: options.packageManager as PackageManager | undefined,
103
103
  skills: options.skills,
104
- graft: options.graft,
104
+ trail: options.trail,
105
105
  git: options.git,
106
106
  dryRun: options.dryRun,
107
107
  profile: profileFlags(options)
@@ -117,7 +117,7 @@ export function createCliProgram(): Command {
117
117
  .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing', 'none')
118
118
  .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun', 'pnpm')
119
119
  .option('--no-skills', 'Skip installing AI skills')
120
- .option('--no-graft', 'Skip codebase graph (Graft) setup')
120
+ .option('--no-trail', 'Skip codebase graph (Trail) setup')
121
121
  .option('--no-git', 'Skip git repository initialization')
122
122
  .option('--dry-run', 'Simulate creation without writing changes to disk');
123
123
  withProfileOptions(newCommand)
@@ -159,7 +159,7 @@ export function createCliProgram(): Command {
159
159
  framework: finalFramework as WebFramework,
160
160
  packageManager: finalPackageManager as PackageManager,
161
161
  skills: options.skills,
162
- graft: options.graft,
162
+ trail: options.trail,
163
163
  git: options.git,
164
164
  dryRun: options.dryRun,
165
165
  profile: finalProfile
@@ -175,7 +175,7 @@ export function createCliProgram(): Command {
175
175
  .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing')
176
176
  .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun')
177
177
  .option('--no-skills', 'Skip installing AI skills')
178
- .option('--no-graft', 'Skip codebase graph (Graft) setup')
178
+ .option('--no-trail', 'Skip codebase graph (Trail) setup')
179
179
  .option('--no-git', 'Skip git repository initialization')
180
180
  .option('--dry-run', 'Simulate setup without writing changes to disk')
181
181
  .option('-y, --yes', 'Skip the prompts and use saved or detected settings');
@@ -209,7 +209,7 @@ export function createCliProgram(): Command {
209
209
  framework: finalFramework as WebFramework,
210
210
  packageManager: finalPackageManager as PackageManager,
211
211
  skills: options.skills,
212
- graft: options.graft,
212
+ trail: options.trail,
213
213
  git: options.git,
214
214
  dryRun: options.dryRun,
215
215
  profile: finalProfile
@@ -278,16 +278,16 @@ export function createCliProgram(): Command {
278
278
  const advice = ignoreVendoredSkills(targetDir, { dryRun: options.dryRun, silent: true });
279
279
  for (const line of advice) logger.warn(line);
280
280
  }
281
- const graftOk =
282
- config?.codebase.graft === false
281
+ const trailOk =
282
+ config?.codebase.trail === false
283
283
  ? true
284
- : await buildGraft(targetDir, { dryRun: options.dryRun });
285
- if (skillsOk && graftOk) logger.success('Update finished.');
284
+ : await buildTrail(targetDir, { dryRun: options.dryRun });
285
+ if (skillsOk && trailOk) logger.success('Update finished.');
286
286
  else {
287
287
  if (!skillsOk) logger.warn('Skills update failed. Check connectivity and the skills CLI.');
288
- if (!graftOk)
288
+ if (!trailOk)
289
289
  logger.warn(
290
- 'Graft refresh failed or Graft is unavailable. Check installation and rerun graft build.'
290
+ 'Trail refresh failed or Trail is unavailable. Check installation and rerun trail build.'
291
291
  );
292
292
  process.exitCode = 1;
293
293
  }
package/src/cli/new.ts CHANGED
@@ -50,7 +50,7 @@ export interface NewProjectOptions {
50
50
  framework?: WebFramework;
51
51
  packageManager?: PackageManager;
52
52
  skills?: boolean;
53
- graft?: boolean;
53
+ trail?: boolean;
54
54
  git?: boolean;
55
55
  dryRun?: boolean;
56
56
  targetDir?: string;
@@ -179,7 +179,7 @@ export async function createProject(
179
179
  const manifest = readPackageManifest(targetDir); // Validate before creating or modifying anything.
180
180
  const config = resolveConfig(projectName, targetDir, options, existingConfig, manifest);
181
181
  if (config.preset !== 'web') delete config.framework;
182
- if (options.graft === false) config.codebase = { ...config.codebase, graft: false };
182
+ if (options.trail === false) config.codebase = { ...config.codebase, trail: false };
183
183
  if (options.skills === false && !existingConfig) config.skills.sources = [];
184
184
  const presetName = config.preset;
185
185
  const packageManager = config.packageManager ?? 'pnpm';
@@ -197,14 +197,14 @@ export async function createProject(
197
197
  let complete = true;
198
198
 
199
199
  const installSkills = options.skills !== false && config.skills.sources.length > 0;
200
- // Upstream setups (agent-browser runtime, e2e, Graft) in registry order, Graft last
200
+ // Upstream setups (agent-browser runtime, e2e, Trail) in registry order, Trail last
201
201
  // so its first index sees every generated file.
202
202
  const setups = active
203
203
  .flatMap((capability) =>
204
204
  capability.install.flatMap((step) => (step.kind === 'setup' ? [step.setup] : []))
205
205
  )
206
- .filter((setup) => !(setup === 'graft' && options.graft === false))
207
- .sort((a, b) => Number(a === 'graft') - Number(b === 'graft'));
206
+ .filter((setup) => !(setup === 'trail' && options.trail === false))
207
+ .sort((a, b) => Number(a === 'trail') - Number(b === 'trail'));
208
208
  const totalSteps = 6 + setups.length;
209
209
  let currentStep = 1;
210
210
 
package/src/cli/wizard.ts CHANGED
@@ -201,16 +201,20 @@ async function promptProfile(
201
201
  return { ...flags, platform, methodology, design, testing: testing as ProfileFlags['testing'], uiDriver, addons };
202
202
  }
203
203
 
204
- async function confirmSetup(answers: WizardInitAnswers): Promise<boolean> {
204
+ export async function confirmSetup(answers: WizardInitAnswers): Promise<boolean> {
205
205
  const profile = answers.profile;
206
206
  const name = (id: string | undefined) => CATALOG.find((c) => c.id === id)?.name ?? id;
207
+ const graph = CATALOG.find(
208
+ (c) => c.category === 'codebase-graph' && c.default && !profile.without?.includes(c.id)
209
+ );
207
210
  p.note([
208
211
  `Platform: ${profile.platform}`,
209
212
  `Methodology: ${name(profile.methodology)}`,
210
213
  `Design: ${name(profile.design)}`,
211
214
  `Testing: ${name(profile.testing)}`,
212
215
  `UI driver: ${name(profile.uiDriver)}`,
213
- `Add-ons: ${profile.addons?.map(name).join(', ') || 'None'}`
216
+ `Add-ons: ${profile.addons?.map(name).join(', ') || 'None'}`,
217
+ `Codebase graph: ${graph ? `${graph.name}. ${graph.description}` : 'None'}`
214
218
  ].join('\n'), 'Setup');
215
219
  return assertNotCancelled(await p.confirm({ message: 'Apply this setup?', initialValue: true }));
216
220
  }
@@ -231,7 +235,7 @@ export async function runInteractiveWizard(): Promise<void> {
231
235
  {
232
236
  value: 'init',
233
237
  label: 'Current directory',
234
- hint: 'Add AGENTS.md, skills & Graft safely'
238
+ hint: 'Add AGENTS.md, skills & Trail safely'
235
239
  }
236
240
  ]
237
241
  })
@@ -63,7 +63,7 @@ export function getDefaultConfig(
63
63
  claudeCode: true
64
64
  },
65
65
  skills: { sources: [] },
66
- codebase: { graft: true },
66
+ codebase: { trail: true },
67
67
  browser: { visualAgent: false, playwright: false },
68
68
  docs: { adr: true },
69
69
  github: { ci: preset !== 'generic' }
@@ -19,7 +19,7 @@ export interface AginitConfig {
19
19
  primary: string;
20
20
  secondary: string;
21
21
  /**
22
- * Also install skills, Graft wiring and a CLAUDE.md import for Claude Code.
22
+ * Also install skills, Trail wiring and a CLAUDE.md import for Claude Code.
23
23
  * Missing in configs written before 0.4.0; treated as enabled.
24
24
  */
25
25
  claudeCode?: boolean;
@@ -34,7 +34,7 @@ export interface AginitConfig {
34
34
  sources: SkillSourceConfig[];
35
35
  };
36
36
  codebase: {
37
- graft: boolean;
37
+ trail: boolean;
38
38
  };
39
39
  browser: {
40
40
  visualAgent: boolean;
@@ -50,11 +50,25 @@ export function validateUpstreamArgument(value: unknown, label: string): asserts
50
50
  );
51
51
  }
52
52
  }
53
- export function validateConfig(value: unknown): AginitConfig {
53
+ /** Configs saved before Graft became Trail name it `graft`; read them as `trail`. */
54
+ function migrateGraftConfig(value: Record<string, any>): Record<string, any> {
55
+ const rename = (ids: unknown) =>
56
+ Array.isArray(ids) ? ids.map((id) => (id === 'graft' ? 'trail' : id)) : ids;
57
+ const { graft, ...codebase } = isRecord(value.codebase) ? value.codebase : {};
58
+ return {
59
+ ...value,
60
+ ...(isRecord(value.codebase) ? { codebase: { trail: graft, ...codebase } } : {}),
61
+ ...(value.capabilities ? { capabilities: rename(value.capabilities) } : {}),
62
+ ...(value.disabledCapabilities ? { disabledCapabilities: rename(value.disabledCapabilities) } : {})
63
+ };
64
+ }
65
+
66
+ export function validateConfig(input: unknown): AginitConfig {
54
67
  const fail = (field: string): never => {
55
68
  throw new Error(`Invalid project configuration: ${field}`);
56
69
  };
57
- if (!isRecord(value)) return fail('expected a JSON object');
70
+ if (!isRecord(input)) return fail('expected a JSON object');
71
+ const value = migrateGraftConfig(input);
58
72
  if (value.schemaVersion !== undefined && value.schemaVersion !== '1.0.0')
59
73
  fail('unsupported schemaVersion');
60
74
  if (typeof value.name !== 'string' || !value.name) fail('name');
@@ -112,7 +126,7 @@ export function validateConfig(value: unknown): AginitConfig {
112
126
  }
113
127
  }
114
128
  for (const [section, keys] of Object.entries({
115
- codebase: ['graft'],
129
+ codebase: ['trail'],
116
130
  browser: ['visualAgent', 'playwright'],
117
131
  docs: ['adr'],
118
132
  github: ['ci']
@@ -74,6 +74,14 @@ export function detectProject(dir: string): DetectedProject {
74
74
  return { platform: null, stack: 'unknown' };
75
75
  }
76
76
 
77
+ /** The file configuring ruff, in ruff's own lookup order, when the project uses it. */
78
+ export function ruffConfigFile(dir: string): string | undefined {
79
+ const local = ['.ruff.toml', 'ruff.toml'].find((file) => fs.existsSync(path.join(dir, file)));
80
+ if (local) return local;
81
+ const pyproject = readTextFile(path.join(dir, 'pyproject.toml')) ?? '';
82
+ return /^\[tool\.ruff[.\]]/m.test(pyproject) ? 'pyproject.toml' : undefined;
83
+ }
84
+
77
85
  /**
78
86
  * The project's own commands: package scripts for Node projects, the host
79
87
  * toolchain otherwise. Commands that would be guesses are left out.
@@ -126,8 +134,13 @@ export function projectCommands(
126
134
  return { build: 'cargo build', typecheck: 'cargo check', lint: 'cargo clippy', test: 'cargo test' };
127
135
  case 'go':
128
136
  return { build: 'go build ./...', lint: 'go vet ./...', test: 'go test ./...' };
129
- case 'python':
130
- return { test: exists('uv.lock') ? 'uv run pytest' : 'pytest' };
137
+ case 'python': {
138
+ const run = exists('uv.lock') ? 'uv run ' : '';
139
+ return {
140
+ ...(ruffConfigFile(dir) ? { lint: `${run}ruff check . && ${run}ruff format --check .` } : {}),
141
+ test: `${run}pytest`
142
+ };
143
+ }
131
144
  case 'unknown':
132
145
  return {};
133
146
  }
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export * from './presets/types.js';
6
6
  export * from './adapters/package-manager.js';
7
7
  export * from './adapters/git.js';
8
8
  export * from './adapters/skills.js';
9
- export * from './adapters/graft.js';
9
+ export * from './adapters/trail.js';
10
10
  export * from './adapters/agents-md.js';
11
11
  export * from './adapters/browser.js';
12
12
  export * from './adapters/agent-browser.js';
@@ -68,29 +68,31 @@ const uiDriverName = (ctx: GuideContext) =>
68
68
  export const CATALOG: CapabilityDescriptor[] = [
69
69
  // ── Codebase graph ────────────────────────────────────────────────────────
70
70
  {
71
- id: 'graft',
72
- name: 'Graft',
73
- description: 'Codebase context graph: graft map / ask --source / skeleton / callers',
71
+ id: 'trail',
72
+ name: 'Trail',
73
+ description:
74
+ 'Codebase graph and team learnings (trail ask / skeleton / callers). ' +
75
+ 'Sends anonymous usage stats: opt out with `trail telemetry disable` or DO_NOT_TRACK=1',
74
76
  category: 'codebase-graph',
75
- author: 'NanoNets / Trail (graft contributors)',
77
+ author: 'Trail (formerly Graft, by NanoNets)',
76
78
  repoUrl: 'https://github.com/trailhq/Trail-Code',
77
79
  license: 'MIT',
78
80
  supportedPlatforms: ['all'],
79
81
  requiresNodeInProjectRoot: false,
80
82
  default: true,
81
83
  install: [
82
- { kind: 'binary', bin: 'graft', install: 'npm install -g @nanonets/graft' },
83
- { kind: 'setup', setup: 'graft' }
84
+ { kind: 'binary', bin: 'trail', install: 'npm install -g @trailhq/trail-code' },
85
+ { kind: 'setup', setup: 'trail' }
84
86
  ],
85
- doctor: [{ kind: 'file', path: 'graft/INDEX.md', fix: 'graft build' }],
87
+ doctor: [{ kind: 'file', path: '.trail/cache/INDEX.md', fix: 'trail build' }],
86
88
  routing: {
87
89
  task: 'Codebase exploration & impact analysis',
88
- read: '`graft ask "<query>" --source` / `graft skeleton <file>` (see the Graft section)'
90
+ read: '`trail ask "<query>" --source` / `trail skeleton <file>` (see the Trail section)'
89
91
  },
90
92
  rebuttals: [
91
93
  {
92
94
  excuse: "I'll grep/read the whole folder to understand the module.",
93
- rule: 'Forbidden. Query `graft ask "<query>" --source` or `graft callers <symbol>` first to save context tokens.'
95
+ rule: 'Forbidden. Query `trail ask "<query>" --source` or `trail callers <symbol>` first to save context tokens.'
94
96
  }
95
97
  ]
96
98
  },
@@ -442,7 +444,10 @@ export const CATALOG: CapabilityDescriptor[] = [
442
444
  `## Unit & integration tests: ${ctx.stack} toolchain`,
443
445
  `- Run: ${code(ctx.commands.test, 'the project test command')}`,
444
446
  ctx.commands.lint ? `- Static checks: \`${ctx.commands.lint}\`` : undefined,
445
- '- Write the failing test first, then the code. Keep tests next to the module they cover, following the toolchain convention.'
447
+ '- Write the failing test first, then the code. ' +
448
+ (ctx.testDir
449
+ ? `Put tests in \`${ctx.testDir}/\`, mirroring the module they cover.`
450
+ : 'Place tests where the toolchain convention puts them.')
446
451
  ]
447
452
  .filter((line): line is string => line !== undefined)
448
453
  .join('\n')
@@ -264,11 +264,11 @@ function mergeSources(
264
264
 
265
265
  /**
266
266
  * Capabilities a config written before v2 (no `capabilities` field) uses:
267
- * the ones whose skills it installs, plus Graft and its test harness.
267
+ * the ones whose skills it installs, plus Trail and its test harness.
268
268
  */
269
269
  export function inferCapabilities(config: AginitConfig): string[] {
270
270
  const ids: string[] = [];
271
- if (config.codebase.graft) ids.push('graft');
271
+ if (config.codebase.trail) ids.push('trail');
272
272
  for (const capability of CATALOG) {
273
273
  const steps = skillSteps(capability);
274
274
  if (
@@ -297,7 +297,7 @@ export function activeCapabilities(config: AginitConfig): CapabilityDescriptor[]
297
297
  .filter((c): c is CapabilityDescriptor => c !== undefined)
298
298
  .filter((capability) => {
299
299
  // The legacy switches still turn their capability off.
300
- if (capability.id === 'graft' && config.codebase.graft === false) return false;
300
+ if (capability.id === 'trail' && config.codebase.trail === false) return false;
301
301
  if (capability.id === 'playwright' && !config.browser.playwright) return false;
302
302
  // Skills only install from configured sources (`--no-skills` empties them).
303
303
  const steps = skillSteps(capability).filter((step) => !step.optional);
@@ -314,7 +314,7 @@ export function activeIds(config: AginitConfig): Set<string> {
314
314
 
315
315
  /** Keep the pre-v2 switches in step with the capability list. */
316
316
  function mirrorLegacySwitches(config: AginitConfig, ids: string[]): void {
317
- config.codebase = { ...config.codebase, graft: ids.includes('graft') };
317
+ config.codebase = { ...config.codebase, trail: ids.includes('trail') };
318
318
  config.browser = {
319
319
  ...config.browser,
320
320
  visualAgent: ids.includes('agent-browser'),
@@ -1,5 +1,5 @@
1
1
  import { AginitConfig } from '../config/schema.js';
2
- import { initGraft } from '../adapters/graft.js';
2
+ import { initTrail } from '../adapters/trail.js';
3
3
  import { setupAgentBrowser } from '../adapters/agent-browser.js';
4
4
  import { setupE2e } from '../adapters/e2e.js';
5
5
  import { claudeCodeEnabled } from '../adapters/skills.js';
@@ -16,8 +16,8 @@ export interface SetupContext {
16
16
 
17
17
  /** Upstream setup commands, keyed by the `setup` install step that names them. */
18
18
  export const SETUPS: Record<SetupId, (ctx: SetupContext) => Promise<boolean>> = {
19
- graft: (ctx) =>
20
- initGraft(ctx.targetDir, {
19
+ trail: (ctx) =>
20
+ initTrail(ctx.targetDir, {
21
21
  dryRun: ctx.dryRun,
22
22
  silent: ctx.silent,
23
23
  claude: claudeCodeEnabled(ctx.config)
@@ -34,7 +34,7 @@ export const SETUPS: Record<SetupId, (ctx: SetupContext) => Promise<boolean>> =
34
34
  };
35
35
 
36
36
  export const SETUP_LABELS: Record<SetupId, string> = {
37
- graft: 'Configuring codebase intelligence (Graft)',
37
+ trail: 'Configuring codebase intelligence (Trail)',
38
38
  'agent-browser': 'Provisioning agent-browser runtime',
39
39
  e2e: 'Scaffolding e2e tests'
40
40
  };
@@ -14,7 +14,7 @@ export type CapabilityCategory =
14
14
  | 'methodology' // pocock, superpowers, minimal
15
15
  | 'discipline' // ponytail, karpathy-guidelines, humanizer
16
16
  | 'design' // impeccable
17
- | 'codebase-graph' // graft
17
+ | 'codebase-graph' // trail
18
18
  | 'ui-driver' // agent-browser, agent-device, chrome-devtools-mcp, browserskill
19
19
  | 'testing-engine' // e2e, playwright, vitest, native
20
20
  | 'context-guard' // context-mode, worktrunk
@@ -39,7 +39,7 @@ export type InstallStep =
39
39
  * Setups that run an upstream init command. vitest and playwright have none:
40
40
  * the presets add them to package.json when they are active.
41
41
  */
42
- export type SetupId = 'graft' | 'agent-browser' | 'e2e';
42
+ export type SetupId = 'trail' | 'agent-browser' | 'e2e';
43
43
 
44
44
  export type DoctorCheck =
45
45
  | { kind: 'file'; path: string; fix: string }
@@ -55,6 +55,8 @@ export interface GuideContext {
55
55
  commands: ProjectCommands;
56
56
  /** Directory that holds the e2e workspace ('' for the project root). */
57
57
  e2eDir: string;
58
+ /** The project's top-level test directory (tests/ or test/), when it has one. */
59
+ testDir?: string;
58
60
  }
59
61
 
60
62
  export interface ProjectCommands {
@@ -1,15 +0,0 @@
1
- export declare function isGraftInstalled(): Promise<boolean>;
2
- export declare function initGraft(targetDir: string, options?: {
3
- dryRun?: boolean;
4
- silent?: boolean;
5
- claude?: boolean;
6
- }): Promise<boolean>;
7
- export declare function buildGraft(targetDir: string, options?: {
8
- dryRun?: boolean;
9
- silent?: boolean;
10
- }): Promise<boolean>;
11
- export declare function uninstallGraft(targetDir: string, options?: {
12
- dryRun?: boolean;
13
- silent?: boolean;
14
- }): Promise<boolean>;
15
- //# sourceMappingURL=graft.d.ts.map