From 0031ec8354949e3e62fec6173f1a809a8203a034 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:49:38 -0700 Subject: [PATCH 1/3] fix: preserve decisions.md and team.md content during export/import Export now includes decisions.md and team.md content in the manifest, and import writes that content back instead of empty strings. Also fixes a duplicate 'hasForce' variable declaration in index.cjs that caused a SyntaxError at runtime. Closes #1122 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- index.cjs | 16 ++++++++++++---- packages/squad-cli/src/cli/commands/export.ts | 15 ++++++++++++++- packages/squad-cli/src/cli/commands/import.ts | 8 +++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/index.cjs b/index.cjs index fbdbafcb3..7b39e0765 100644 --- a/index.cjs +++ b/index.cjs @@ -1027,6 +1027,15 @@ if (cmd === 'export') { skills: [] }; + // Read top-level squad files (decisions.md, team.md) + const decisionsMd = path.join(dest, '.ai-team', 'decisions.md'); + if (fs.existsSync(decisionsMd)) { + manifest.decisions = fs.readFileSync(decisionsMd, 'utf8'); + } + if (fs.existsSync(teamMd)) { + manifest.team = fs.readFileSync(teamMd, 'utf8'); + } + // Read casting state const castingDir = path.join(dest, '.ai-team', 'casting'); for (const file of ['registry.json', 'policy.json', 'history.json']) { @@ -1147,9 +1156,9 @@ if (cmd === 'import') { fs.mkdirSync(path.join(aiTeamDir, 'log'), { recursive: true }); fs.mkdirSync(path.join(aiTeamDir, 'skills'), { recursive: true }); - // Write empty project-specific files - fs.writeFileSync(path.join(aiTeamDir, 'decisions.md'), ''); - fs.writeFileSync(path.join(aiTeamDir, 'team.md'), ''); + // Write project-specific files from manifest (fall back to empty if not present) + fs.writeFileSync(path.join(aiTeamDir, 'decisions.md'), manifest.decisions || ''); + fs.writeFileSync(path.join(aiTeamDir, 'team.md'), manifest.team || ''); // Write casting state for (const [key, value] of Object.entries(manifest.casting)) { @@ -1189,7 +1198,6 @@ if (cmd === 'import') { : `skill-${index}`; return { skillContent, skillName }; }); - const hasForce = typeof force !== 'undefined' && force; if (fs.existsSync(copilotSkillsImportDir)) { if (hasForce) { diff --git a/packages/squad-cli/src/cli/commands/export.ts b/packages/squad-cli/src/cli/commands/export.ts index 41216bb66..72078aa35 100644 --- a/packages/squad-cli/src/cli/commands/export.ts +++ b/packages/squad-cli/src/cli/commands/export.ts @@ -16,6 +16,8 @@ interface ExportManifest { casting: Record; agents: Record; skills: string[]; + decisions?: string; + team?: string; } /** @@ -36,9 +38,20 @@ export async function runExport(dest: string, outPath?: string): Promise { squad_version: '0.6.0', casting: {}, agents: {}, - skills: [] + skills: [], }; + // Read top-level squad files (decisions.md, team.md) + const decisionsMd = path.join(squadInfo.path, 'decisions.md'); + const decisionsContent = storage.readSync(decisionsMd); + if (decisionsContent !== undefined) { + manifest.decisions = decisionsContent; + } + const teamContent = storage.readSync(teamMd); + if (teamContent !== undefined) { + manifest.team = teamContent; + } + // Read casting state const castingDir = path.join(squadInfo.path, 'casting'); for (const file of ['registry.json', 'policy.json', 'history.json']) { diff --git a/packages/squad-cli/src/cli/commands/import.ts b/packages/squad-cli/src/cli/commands/import.ts index da61ad928..af9ed7c38 100644 --- a/packages/squad-cli/src/cli/commands/import.ts +++ b/packages/squad-cli/src/cli/commands/import.ts @@ -17,6 +17,8 @@ interface ImportManifest { casting: Record; agents: Record; skills: string[]; + decisions?: string; + team?: string; } /** @@ -77,9 +79,9 @@ export async function runImport(dest: string, importPath: string, force: boolean storage.mkdirSync(path.join(squadDir, 'log'), { recursive: true }); storage.mkdirSync(path.join(dest, '.copilot', 'skills'), { recursive: true }); - // Write empty project-specific files - storage.writeSync(path.join(squadDir, 'decisions.md'), ''); - storage.writeSync(path.join(squadDir, 'team.md'), ''); + // Write project-specific files from manifest (fall back to empty if not present) + storage.writeSync(path.join(squadDir, 'decisions.md'), manifest.decisions || ''); + storage.writeSync(path.join(squadDir, 'team.md'), manifest.team || ''); // Write casting state for (const [key, value] of Object.entries(manifest.casting)) { From e3b880f81646f604b0f7e1c8814fc3be7df3ab2f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:49:56 -0700 Subject: [PATCH 2/3] chore: add changeset for export/import fix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/fix-export-import-blank-files.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-export-import-blank-files.md diff --git a/.changeset/fix-export-import-blank-files.md b/.changeset/fix-export-import-blank-files.md new file mode 100644 index 000000000..4a27cfeef --- /dev/null +++ b/.changeset/fix-export-import-blank-files.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-cli": patch +--- + +Fix export/import producing blank decisions.md and team.md files. Export now includes these files in the manifest, and import restores their content. From c83e5d663472a0d08de27ac214bc75e8e39bbaa3 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 10:14:35 -0700 Subject: [PATCH 3/3] fix: address PR review comments on export/import - Add validation that manifest.decisions and manifest.team are strings - Replace hard-coded squad_version '0.6.0' with getPackageVersion() - Update export warning to mention decisions/team content - Add test coverage for decisions.md/team.md round-trip and backward compatibility with older manifests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- index.cjs | 10 ++- packages/squad-cli/src/cli/commands/export.ts | 5 +- packages/squad-cli/src/cli/commands/import.ts | 8 ++ test/skills-export-import.test.cjs | 85 +++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/index.cjs b/index.cjs index 7b39e0765..f3c90c9f2 100644 --- a/index.cjs +++ b/index.cjs @@ -1099,7 +1099,7 @@ if (cmd === 'export') { const displayPath = path.relative(dest, outPath) || path.basename(outPath); console.log(`${GREEN}✓${RESET} Exported squad to ${displayPath}`); - console.log(`${DIM}⚠ Review agent histories before sharing — they may contain project-specific information${RESET}`); + console.log(`${DIM}⚠ Review agent histories, decisions, and team content before sharing — they may contain project-specific information${RESET}`); process.exit(0); } @@ -1156,6 +1156,14 @@ if (cmd === 'import') { fs.mkdirSync(path.join(aiTeamDir, 'log'), { recursive: true }); fs.mkdirSync(path.join(aiTeamDir, 'skills'), { recursive: true }); + // Validate optional string fields + if (manifest.decisions !== undefined && typeof manifest.decisions !== 'string') { + fatal('Invalid export file: "decisions" field must be a string'); + } + if (manifest.team !== undefined && typeof manifest.team !== 'string') { + fatal('Invalid export file: "team" field must be a string'); + } + // Write project-specific files from manifest (fall back to empty if not present) fs.writeFileSync(path.join(aiTeamDir, 'decisions.md'), manifest.decisions || ''); fs.writeFileSync(path.join(aiTeamDir, 'team.md'), manifest.team || ''); diff --git a/packages/squad-cli/src/cli/commands/export.ts b/packages/squad-cli/src/cli/commands/export.ts index 72078aa35..fd9a50070 100644 --- a/packages/squad-cli/src/cli/commands/export.ts +++ b/packages/squad-cli/src/cli/commands/export.ts @@ -8,6 +8,7 @@ import { FSStorageProvider } from '@bradygaster/squad-sdk'; import { detectSquadDir } from '../core/detect-squad-dir.js'; import { success, warn } from '../core/output.js'; import { fatal } from '../core/errors.js'; +import { getPackageVersion } from '../core/version.js'; interface ExportManifest { version: string; @@ -35,7 +36,7 @@ export async function runExport(dest: string, outPath?: string): Promise { const manifest: ExportManifest = { version: '1.0', exported_at: new Date().toISOString(), - squad_version: '0.6.0', + squad_version: getPackageVersion(), casting: {}, agents: {}, skills: [], @@ -122,5 +123,5 @@ export async function runExport(dest: string, outPath?: string): Promise { const displayPath = path.relative(dest, finalOutPath) || path.basename(finalOutPath); success(`Exported squad to ${displayPath}`); - warn('Review agent histories before sharing — they may contain project-specific information'); + warn('Review agent histories, decisions, and team content before sharing — they may contain project-specific information'); } diff --git a/packages/squad-cli/src/cli/commands/import.ts b/packages/squad-cli/src/cli/commands/import.ts index af9ed7c38..33f137c77 100644 --- a/packages/squad-cli/src/cli/commands/import.ts +++ b/packages/squad-cli/src/cli/commands/import.ts @@ -79,6 +79,14 @@ export async function runImport(dest: string, importPath: string, force: boolean storage.mkdirSync(path.join(squadDir, 'log'), { recursive: true }); storage.mkdirSync(path.join(dest, '.copilot', 'skills'), { recursive: true }); + // Validate optional string fields + if (manifest.decisions !== undefined && typeof manifest.decisions !== 'string') { + fatal('Invalid export file: "decisions" field must be a string'); + } + if (manifest.team !== undefined && typeof manifest.team !== 'string') { + fatal('Invalid export file: "team" field must be a string'); + } + // Write project-specific files from manifest (fall back to empty if not present) storage.writeSync(path.join(squadDir, 'decisions.md'), manifest.decisions || ''); storage.writeSync(path.join(squadDir, 'team.md'), manifest.team || ''); diff --git a/test/skills-export-import.test.cjs b/test/skills-export-import.test.cjs index 8e4e673a2..f9a1f5e68 100644 --- a/test/skills-export-import.test.cjs +++ b/test/skills-export-import.test.cjs @@ -262,3 +262,88 @@ name: report-test assert.ok(importResult.stdout.includes('skill'), 'import output should mention skills'); }); }); + +describe('decisions.md and team.md survive export/import round-trip (#1122)', () => { + let tmpDir1; + let tmpDir2; + + beforeEach(() => { + tmpDir1 = makeTempDir(); + tmpDir2 = makeTempDir(); + }); + + afterEach(() => { + cleanDir(tmpDir1); + cleanDir(tmpDir2); + }); + + it('exports decisions.md and team.md content in manifest', () => { + initSquad(tmpDir1); + + // Write content to decisions.md and team.md + const aiTeamDir = path.join(tmpDir1, '.ai-team'); + fs.writeFileSync(path.join(aiTeamDir, 'decisions.md'), '# Decisions\n\n## Use TypeScript\nWe chose TypeScript for type safety.\n'); + fs.writeFileSync(path.join(aiTeamDir, 'team.md'), '# Team Roster\n\n## Lead Architect\nDesigns systems.\n'); + + // Export + const exportPath = path.join(tmpDir1, 'squad-export.json'); + const exportResult = runSquad(['export'], tmpDir1); + assert.equal(exportResult.exitCode, 0, `export should succeed: ${exportResult.stdout}`); + + // Verify manifest contains decisions and team fields + const manifest = JSON.parse(fs.readFileSync(exportPath, 'utf8')); + assert.equal(manifest.decisions, '# Decisions\n\n## Use TypeScript\nWe chose TypeScript for type safety.\n', 'manifest should contain decisions.md content'); + assert.equal(manifest.team, '# Team Roster\n\n## Lead Architect\nDesigns systems.\n', 'manifest should contain team.md content'); + }); + + it('imports decisions.md and team.md content from manifest', () => { + initSquad(tmpDir1); + + // Write content to decisions.md and team.md + const aiTeamDir = path.join(tmpDir1, '.ai-team'); + fs.writeFileSync(path.join(aiTeamDir, 'decisions.md'), '# Decisions\n\nImportant decision here.\n'); + fs.writeFileSync(path.join(aiTeamDir, 'team.md'), '# Team\n\nTeam member info here.\n'); + + // Export from dir1 + const exportPath = path.join(tmpDir1, 'squad-export.json'); + runSquad(['export'], tmpDir1); + + // Init and import into dir2 + initSquad(tmpDir2); + const importResult = runSquad(['import', exportPath, '--force'], tmpDir2); + assert.equal(importResult.exitCode, 0, `import should succeed: ${importResult.stdout}`); + + // Verify imported files have the correct content + const importedDecisions = fs.readFileSync(path.join(tmpDir2, '.ai-team', 'decisions.md'), 'utf8'); + const importedTeam = fs.readFileSync(path.join(tmpDir2, '.ai-team', 'team.md'), 'utf8'); + assert.equal(importedDecisions, '# Decisions\n\nImportant decision here.\n', 'decisions.md should be preserved'); + assert.equal(importedTeam, '# Team\n\nTeam member info here.\n', 'team.md should be preserved'); + }); + + it('imports older manifests without decisions/team fields successfully', () => { + initSquad(tmpDir1); + + // Create a manifest without decisions and team fields (older format) + const oldManifest = { + version: '1.0', + exported_at: new Date().toISOString(), + squad_version: '0.6.0', + casting: { registry: { agents: [] } }, + agents: {}, + skills: [] + }; + const exportPath = path.join(tmpDir1, 'old-manifest.json'); + fs.writeFileSync(exportPath, JSON.stringify(oldManifest, null, 2)); + + // Init and import into dir2 + initSquad(tmpDir2); + const importResult = runSquad(['import', exportPath, '--force'], tmpDir2); + assert.equal(importResult.exitCode, 0, `import of old manifest should succeed: ${importResult.stdout}`); + + // Verify files exist (empty is fine for old manifests) + const importedDecisions = fs.readFileSync(path.join(tmpDir2, '.ai-team', 'decisions.md'), 'utf8'); + const importedTeam = fs.readFileSync(path.join(tmpDir2, '.ai-team', 'team.md'), 'utf8'); + assert.equal(importedDecisions, '', 'decisions.md should be empty for old manifest'); + assert.equal(importedTeam, '', 'team.md should be empty for old manifest'); + }); +});