Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
getPythonCommandForPlatform,
setResolvedPythonCommand,
} from "../configurators/shared.js";
import { AI_TOOLS, type CliFlag } from "../types/ai-tools.js";
import {
AI_TOOLS,
type CliFlag,
type RootInstructionFile,
} from "../types/ai-tools.js";
import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
import { VERSION } from "../constants/version.js";
import { agentsMdContent } from "../templates/markdown/index.js";
Expand Down Expand Up @@ -844,8 +848,16 @@ async function handleReinit(
}
}

const rootInstructionFilesToCreate =
getRootInstructionFilesForTools(platformsToAdd);
const rootInstructionFilesToHash = getRootInstructionFilesForTools([
...[...configuredPlatforms].map((id) => AI_TOOLS[id].cliFlag),
...platformsToAdd,
]);
await createRootFiles(cwd, rootInstructionFilesToCreate);

// Update template hashes
const hashedCount = initializeHashes(cwd);
const hashedCount = initializeHashes(cwd, rootInstructionFilesToHash);
if (hashedCount > 0) {
console.log(
chalk.gray(`📋 Tracking ${hashedCount} template files for updates`),
Expand Down Expand Up @@ -1763,11 +1775,12 @@ export async function init(options: InitOptions): Promise<void> {
logPythonAdaptationNotice(pythonCmd);
}

// Create root files (skip if exists)
await createRootFiles(cwd);
// Create root instruction files required by selected platforms.
const rootInstructionFiles = getRootInstructionFilesForTools(tools);
await createRootFiles(cwd, rootInstructionFiles);

// Initialize template hashes for modification tracking
const hashedCount = initializeHashes(cwd);
const hashedCount = initializeHashes(cwd, rootInstructionFiles);
if (hashedCount > 0) {
console.log(
chalk.gray(`📋 Tracking ${hashedCount} template files for updates`),
Expand Down Expand Up @@ -1848,12 +1861,28 @@ function askInput(prompt: string): Promise<string> {
});
}

async function createRootFiles(cwd: string): Promise<void> {
const agentsPath = path.join(cwd, FILE_NAMES.AGENTS);
function getRootInstructionFilesForTools(
tools: string[],
): RootInstructionFile[] {
const files = new Set<RootInstructionFile>();
for (const tool of tools) {
const platformId = resolveCliFlag(tool);
if (platformId) {
files.add(AI_TOOLS[platformId].rootInstructionFile);
}
}
return [...files];
}

// Write AGENTS.md from template
const agentsWritten = await writeFile(agentsPath, agentsMdContent);
if (agentsWritten) {
console.log(chalk.blue("📄 Created AGENTS.md"));
async function createRootFiles(
cwd: string,
rootInstructionFiles: RootInstructionFile[],
): Promise<void> {
for (const fileName of rootInstructionFiles) {
const filePath = path.join(cwd, fileName);
const written = await writeFile(filePath, agentsMdContent);
if (written) {
console.log(chalk.blue(`📄 Created ${fileName}`));
}
}
}
46 changes: 35 additions & 11 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import chalk from "chalk";
import inquirer from "inquirer";

import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
import type { AITool } from "../types/ai-tools.js";
import {
AI_TOOLS,
type AITool,
type RootInstructionFile,
} from "../types/ai-tools.js";
import { VERSION, PACKAGE_NAME } from "../constants/version.js";
import {
getMigrationsForVersion,
Expand Down Expand Up @@ -141,8 +145,8 @@ function replaceTrellisManagedBlock(
);
}

function buildAgentsMdTemplate(cwd: string): string {
const fullPath = path.join(cwd, FILE_NAMES.AGENTS);
function buildRootInstructionTemplate(cwd: string, fileName: string): string {
const fullPath = path.join(cwd, fileName);
if (!fs.existsSync(fullPath)) {
return agentsMdContent;
}
Expand Down Expand Up @@ -184,6 +188,16 @@ function isKnownUntrackedTemplate(
return LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES.has(computeHash(managedBlock));
}

function getRootInstructionFilesForPlatforms(
platforms: Set<AITool>,
): RootInstructionFile[] {
const files = new Set<RootInstructionFile>();
for (const platform of platforms) {
files.add(AI_TOOLS[platform].rootInstructionFile);
}
return [...files];
}

/**
* Check if a path is blocked by PROTECTED_PATHS
*/
Expand Down Expand Up @@ -626,6 +640,7 @@ function collectTemplateFiles(
platforms.add(p);
}
}
const rootInstructionFiles = getRootInstructionFilesForPlatforms(platforms);

// Python scripts (single source of truth: getAllScripts())
for (const [scriptPath, content] of getAllScripts()) {
Expand All @@ -645,7 +660,9 @@ function collectTemplateFiles(
files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, workflowMdTemplate);
// workspace/index.md stays excluded — it's runtime-appended by add_session.py
// (journal index) and has no script-parsed structure.
files.set(FILE_NAMES.AGENTS, buildAgentsMdTemplate(cwd));
for (const fileName of rootInstructionFiles) {
files.set(fileName, buildRootInstructionTemplate(cwd, fileName));
}

// Platform-specific templates (only for configured platforms)
for (const platformId of platforms) {
Expand Down Expand Up @@ -760,14 +777,18 @@ function analyzeChanges(
return result;
}

function collectMissingAgentsMdHash(
function collectMissingRootInstructionHashes(
changes: ChangeAnalysis,
hashes: TemplateHashes,
): Map<string, string> {
const files = new Map<string, string>();

for (const file of changes.unchangedFiles) {
if (file.relativePath === FILE_NAMES.AGENTS && !hashes[file.relativePath]) {
if (
(file.relativePath === FILE_NAMES.AGENTS ||
file.relativePath === FILE_NAMES.CLAUDE) &&
!hashes[file.relativePath]
) {
files.set(file.relativePath, file.newContent);
}
}
Expand Down Expand Up @@ -935,7 +956,7 @@ function backupFile(
const BACKUP_DIRS = ALL_MANAGED_DIRS;

/** Root-level managed files to include in update backups. */
const BACKUP_FILES = [FILE_NAMES.AGENTS] as const;
const BACKUP_FILES = [FILE_NAMES.AGENTS, FILE_NAMES.CLAUDE] as const;

/**
* Patterns to exclude from backup (user data that shouldn't be backed up)
Expand Down Expand Up @@ -1944,7 +1965,10 @@ export async function update(options: UpdateOptions): Promise<void> {

// Analyze changes (pass hashes for modification detection)
const changes = analyzeChanges(cwd, hashes, templates);
const missingAgentsMdHash = collectMissingAgentsMdHash(changes, hashes);
const missingRootInstructionHashes = collectMissingRootInstructionHashes(
changes,
hashes,
);

// Print summary
printChangeSummary(changes);
Expand Down Expand Up @@ -1983,8 +2007,8 @@ export async function update(options: UpdateOptions): Promise<void> {
!hasPendingMigrations &&
!hasSafeDeletes
) {
if (!options.dryRun && missingAgentsMdHash.size > 0) {
updateHashes(cwd, missingAgentsMdHash);
if (!options.dryRun && missingRootInstructionHashes.size > 0) {
updateHashes(cwd, missingRootInstructionHashes);
}

if (isSameVersion) {
Expand Down Expand Up @@ -2257,7 +2281,7 @@ export async function update(options: UpdateOptions): Promise<void> {
updateVersionFile(cwd);

// Update template hashes for new, auto-updated, and overwritten files
const filesToHash = new Map<string, string>(missingAgentsMdHash);
const filesToHash = new Map<string, string>(missingRootInstructionHashes);
for (const file of changes.newFiles) {
filesToHash.set(file.relativePath, file.newContent);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/constants/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const DIR_NAMES = {
export const FILE_NAMES = {
/** Root agent instructions file */
AGENTS: "AGENTS.md",
/** Claude Code root instructions file */
CLAUDE: "CLAUDE.md",
/** Developer identity file */
DEVELOPER: ".developer",
/** Current task pointer */
Expand Down
22 changes: 22 additions & 0 deletions packages/cli/src/types/ai-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export type CliFlag =
| "droid"
| "pi";

export type RootInstructionFile = "AGENTS.md" | "CLAUDE.md";

/**
* Template context for placeholder resolution.
* Controls how common templates are rendered per platform.
Expand Down Expand Up @@ -102,6 +104,8 @@ export interface AIToolConfig {
templateDirs: TemplateDir[];
/** Config directory name in the project root (e.g., ".claude") */
configDir: string;
/** Root instructions file managed for this platform */
rootInstructionFile: RootInstructionFile;
/**
* Whether the platform supports the shared `.agents/skills/` layer
* (agentskills.io open standard). When true, `.agents/skills` is added
Expand Down Expand Up @@ -136,6 +140,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Claude Code",
templateDirs: ["common", "claude"],
configDir: ".claude",
rootInstructionFile: "CLAUDE.md",
cliFlag: "claude",
defaultChecked: true,
hasPythonHooks: true,
Expand All @@ -152,6 +157,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Cursor",
templateDirs: ["common", "cursor"],
configDir: ".cursor",
rootInstructionFile: "AGENTS.md",
cliFlag: "cursor",
defaultChecked: true,
hasPythonHooks: true,
Expand All @@ -168,6 +174,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "OpenCode",
templateDirs: ["common", "opencode"],
configDir: ".opencode",
rootInstructionFile: "AGENTS.md",
cliFlag: "opencode",
defaultChecked: false,
hasPythonHooks: false,
Expand All @@ -184,6 +191,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Codex (also writes .agents/skills/ — read by Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code)",
templateDirs: ["common", "codex"],
configDir: ".codex",
rootInstructionFile: "AGENTS.md",
supportsAgentSkills: true,
cliFlag: "codex",
defaultChecked: false,
Expand All @@ -201,6 +209,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Kilo CLI",
templateDirs: ["common", "kilo"],
configDir: ".kilocode",
rootInstructionFile: "AGENTS.md",
cliFlag: "kilo",
defaultChecked: false,
hasPythonHooks: false,
Expand All @@ -217,6 +226,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Kiro Code",
templateDirs: ["common", "kiro"],
configDir: ".kiro/skills",
rootInstructionFile: "AGENTS.md",
extraManagedPaths: [".kiro/agents", ".kiro/hooks"],
cliFlag: "kiro",
defaultChecked: false,
Expand All @@ -234,6 +244,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Gemini CLI",
templateDirs: ["common", "gemini"],
configDir: ".gemini",
rootInstructionFile: "AGENTS.md",
supportsAgentSkills: true,
cliFlag: "gemini",
defaultChecked: false,
Expand All @@ -251,6 +262,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Antigravity",
templateDirs: ["common", "antigravity"],
configDir: ".agent/workflows",
rootInstructionFile: "AGENTS.md",
extraManagedPaths: [".agent/skills"],
cliFlag: "antigravity",
defaultChecked: false,
Expand All @@ -268,6 +280,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Windsurf",
templateDirs: ["common", "windsurf"],
configDir: ".windsurf/workflows",
rootInstructionFile: "AGENTS.md",
extraManagedPaths: [".windsurf/skills"],
cliFlag: "windsurf",
defaultChecked: false,
Expand All @@ -285,6 +298,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Qoder",
templateDirs: ["common", "qoder"],
configDir: ".qoder",
rootInstructionFile: "AGENTS.md",
cliFlag: "qoder",
defaultChecked: false,
hasPythonHooks: true,
Expand All @@ -301,6 +315,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "CodeBuddy",
templateDirs: ["common", "codebuddy"],
configDir: ".codebuddy",
rootInstructionFile: "AGENTS.md",
cliFlag: "codebuddy",
defaultChecked: false,
hasPythonHooks: true,
Expand All @@ -317,6 +332,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "GitHub Copilot",
templateDirs: ["common", "copilot"],
configDir: ".github/copilot",
rootInstructionFile: "AGENTS.md",
extraManagedPaths: [
".github/agents",
".github/hooks",
Expand All @@ -339,6 +355,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Factory Droid",
templateDirs: ["common", "droid"],
configDir: ".factory",
rootInstructionFile: "AGENTS.md",
cliFlag: "droid",
defaultChecked: false,
hasPythonHooks: true,
Expand All @@ -355,6 +372,7 @@ export const AI_TOOLS: Record<AITool, AIToolConfig> = {
name: "Pi Agent",
templateDirs: ["common", "pi"],
configDir: ".pi",
rootInstructionFile: "AGENTS.md",
cliFlag: "pi",
defaultChecked: false,
hasPythonHooks: false,
Expand Down Expand Up @@ -397,3 +415,7 @@ export function getManagedPaths(tool: AITool): string[] {
export function getTemplateDirs(tool: AITool): TemplateDir[] {
return AI_TOOLS[tool].templateDirs;
}

export function getRootInstructionFile(tool: AITool): RootInstructionFile {
return AI_TOOLS[tool].rootInstructionFile;
}
14 changes: 10 additions & 4 deletions packages/cli/src/utils/template-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ export function getModificationStatus(
*/
const TEMPLATE_DIRS = ALL_MANAGED_DIRS;

/** Root-level template files written by init and managed by update. */
const TEMPLATE_FILES = [FILE_NAMES.AGENTS] as const;
/** Root-level template files that may be managed by init/update. */
const TEMPLATE_FILES = new Set<string>([FILE_NAMES.AGENTS, FILE_NAMES.CLAUDE]);

/**
* Patterns to exclude from hash tracking
Expand Down Expand Up @@ -340,10 +340,16 @@ function collectFiles(
* @param cwd - Working directory
* @returns Number of files hashed
*/
export function initializeHashes(cwd: string): number {
export function initializeHashes(
cwd: string,
rootTemplateFiles: readonly string[] = [],
): number {
const hashes: TemplateHashes = {};

for (const relativePath of TEMPLATE_FILES) {
for (const relativePath of new Set(rootTemplateFiles)) {
if (!TEMPLATE_FILES.has(relativePath)) {
continue;
}
if (shouldExcludeFromHash(relativePath)) {
continue;
}
Expand Down
Loading