diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 74b042fea..d8caf1663 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -96,7 +96,7 @@ The `union` merge driver keeps all lines from both sides, which is correct for a **⚠️ CRITICAL RULE: Every agent interaction MUST use the `task` tool to spawn a real agent. You MUST call the `task` tool — never simulate, role-play, or inline an agent's work. If you did not call the `task` tool, the agent was NOT spawned. No exceptions.** -**On every session start:** Run `git config user.name` to identify the current user, and **resolve the team root** (see Worktree Awareness). Store the team root — all `.squad/` paths must be resolved relative to it. Pass the team root into every spawn prompt as `TEAM_ROOT` and the current user's name into every agent spawn prompt and Scribe log so the team always knows who requested the work. +**On every session start:** Run `git config user.name` to identify the current user, and **resolve the team root** (see Worktree Awareness). Store the team root — all `.squad/` paths must be resolved relative to it. Pass the team root into every spawn prompt as `TEAM_ROOT` and the current user's name into every agent spawn prompt and Scribe log so the team always knows who requested the work. Check `.squad/identity/now.md` if it exists — it tells you what the team was last focused on. Update it if the focus has shifted. **⚡ Context caching:** After the first message in a session, `team.md`, `routing.md`, and `registry.json` are already in your context. Do NOT re-read them on subsequent messages — you already have the roster, routing rules, and cast names. Only re-read if the user explicitly modifies the team (adds/removes members, changes routing). @@ -641,6 +641,8 @@ prompt: | Read .squad/agents/{name}/history.md (your project knowledge). Read .squad/decisions.md (team decisions to respect). + If .squad/identity/wisdom.md exists, read it before starting work. + If .squad/identity/now.md exists, read it at spawn time. If .squad/skills/ has relevant SKILL.md files, read them before working. {only if MCP tools detected — omit entirely if none:} diff --git a/index.js b/index.js index 6d091eec1..b5ba50320 100644 --- a/index.js +++ b/index.js @@ -804,6 +804,15 @@ if (isMigrateDirectory) { } } + // Scrub email addresses from migrated files + console.log(`${DIM}Scrubbing email addresses from .squad/ files...${RESET}`); + const scrubbedFiles = scrubEmailsFromDirectory(squadDir); + if (scrubbedFiles.length > 0) { + console.log(`${GREEN}✓${RESET} Scrubbed email addresses from ${scrubbedFiles.length} file(s)`); + } else { + console.log(`${GREEN}✓${RESET} No email addresses found`); + } + console.log(); console.log(`${BOLD}Migration complete.${RESET}`); console.log(`${DIM}Commit the change:${RESET}`); @@ -1039,18 +1048,20 @@ if (squadInfo.isLegacy) { showDeprecationWarning(); } -// Pre-create drop-box, orchestration-log, casting, skills, and plugins directories (additive-only) +// Pre-create drop-box, orchestration-log, casting, skills, plugins, and identity directories (additive-only) const inboxDir = path.join(squadInfo.path, 'decisions', 'inbox'); const orchLogDir = path.join(squadInfo.path, 'orchestration-log'); const castingDir = path.join(squadInfo.path, 'casting'); const skillsDir = path.join(squadInfo.path, 'skills'); const pluginsDir = path.join(squadInfo.path, 'plugins'); +const identityDir = path.join(squadInfo.path, 'identity'); try { fs.mkdirSync(inboxDir, { recursive: true }); fs.mkdirSync(orchLogDir, { recursive: true }); fs.mkdirSync(castingDir, { recursive: true }); fs.mkdirSync(skillsDir, { recursive: true }); fs.mkdirSync(pluginsDir, { recursive: true }); + fs.mkdirSync(identityDir, { recursive: true }); } catch (err) { fatal(`Failed to create ${squadInfo.name}/ directories: ${err.message}`); } @@ -1064,6 +1075,51 @@ if (!isUpgrade) { } } +// Scaffold identity files (now.md, wisdom.md) — both init and upgrade +const nowMdPath = path.join(identityDir, 'now.md'); +const wisdomMdPath = path.join(identityDir, 'wisdom.md'); + +if (!fs.existsSync(nowMdPath)) { + const nowTemplate = `--- +updated_at: ${new Date().toISOString()} +focus_area: Initial setup +active_issues: [] +--- + +# What We're Focused On + +Getting started. Updated by coordinator at session start. +`; + fs.mkdirSync(identityDir, { recursive: true }); + fs.writeFileSync(nowMdPath, nowTemplate); + console.log(`${GREEN}✓${RESET} ${squadInfo.name}/identity/now.md`); +} else if (isUpgrade) { + console.log(`${DIM}identity/now.md already exists — skipping${RESET}`); +} + +if (!fs.existsSync(wisdomMdPath)) { + const wisdomTemplate = `--- +last_updated: ${new Date().toISOString()} +--- + +# Team Wisdom + +Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight. + +## Patterns + + + +## Anti-Patterns + + +`; + fs.mkdirSync(identityDir, { recursive: true }); + fs.writeFileSync(wisdomMdPath, wisdomTemplate); + console.log(`${GREEN}✓${RESET} ${squadInfo.name}/identity/wisdom.md`); +} else if (isUpgrade) { + console.log(`${DIM}identity/wisdom.md already exists — skipping${RESET}`); +} // Create sample MCP config (skip if .copilot/mcp-config.json already exists) if (!isUpgrade) { @@ -1189,6 +1245,15 @@ if (fs.existsSync(workflowsSrc) && fs.statSync(workflowsSrc).isDirectory()) { } if (isUpgrade) { + // Scrub email addresses from existing squad directory + console.log(`${DIM}Scrubbing email addresses from ${squadInfo.name}/ files...${RESET}`); + const scrubResult = scrubEmailsFromDirectory(squadInfo.path); + if (scrubResult.filesModified > 0) { + console.log(`${GREEN}✓${RESET} Scrubbed ${scrubResult.replacements} email address(es) from ${scrubResult.filesModified} file(s)`); + } else { + console.log(`${GREEN}✓${RESET} No email addresses found (scanned ${scrubResult.filesScanned} files)`); + } + console.log(`\n${DIM}${squadInfo.name}/ untouched — your team state is safe${RESET}`); // Hint about new features available after upgrade diff --git a/templates/identity/now.md b/templates/identity/now.md new file mode 100644 index 000000000..04e1dfeeb --- /dev/null +++ b/templates/identity/now.md @@ -0,0 +1,9 @@ +--- +updated_at: {timestamp} +focus_area: {brief description} +active_issues: [] +--- + +# What We're Focused On + +{Narrative description of current focus — 1-3 sentences. Updated by coordinator at session start.} diff --git a/templates/identity/wisdom.md b/templates/identity/wisdom.md new file mode 100644 index 000000000..c3b978e4f --- /dev/null +++ b/templates/identity/wisdom.md @@ -0,0 +1,15 @@ +--- +last_updated: {timestamp} +--- + +# Team Wisdom + +Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight. + +## Patterns + + + +## Anti-Patterns + + diff --git a/test/init-flow.test.js b/test/init-flow.test.js index 269c324d6..72e06e4ba 100644 --- a/test/init-flow.test.js +++ b/test/init-flow.test.js @@ -247,3 +247,67 @@ describe('Init Mode prompt structure (#66)', () => { }); }); }); + +describe('Identity layer files (now.md, wisdom.md) — #107', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = makeTempDir(); + }); + + afterEach(() => { + cleanDir(tmpDir); + }); + + it('init creates .squad/identity/now.md', () => { + initSquad(tmpDir); + const nowPath = path.join(tmpDir, '.squad', 'identity', 'now.md'); + assert.ok(fs.existsSync(nowPath), '.squad/identity/now.md should exist after init'); + + const content = fs.readFileSync(nowPath, 'utf8'); + assert.ok(content.includes('updated_at:'), 'now.md should contain updated_at field'); + assert.ok(content.includes('focus_area:'), 'now.md should contain focus_area field'); + assert.ok(content.includes('What We\'re Focused On'), 'now.md should contain heading'); + }); + + it('init creates .squad/identity/wisdom.md', () => { + initSquad(tmpDir); + const wisdomPath = path.join(tmpDir, '.squad', 'identity', 'wisdom.md'); + assert.ok(fs.existsSync(wisdomPath), '.squad/identity/wisdom.md should exist after init'); + + const content = fs.readFileSync(wisdomPath, 'utf8'); + assert.ok(content.includes('last_updated:'), 'wisdom.md should contain last_updated field'); + assert.ok(content.includes('Team Wisdom'), 'wisdom.md should contain heading'); + assert.ok(content.includes('## Patterns'), 'wisdom.md should contain Patterns section'); + assert.ok(content.includes('## Anti-Patterns'), 'wisdom.md should contain Anti-Patterns section'); + }); + + it('upgrade creates missing identity files', () => { + // First init + initSquad(tmpDir); + + // Modify squad.agent.md to simulate an old version + const agentPath = path.join(tmpDir, '.github', 'agents', 'squad.agent.md'); + let agentContent = fs.readFileSync(agentPath, 'utf8'); + agentContent = agentContent.replace(//, ''); + agentContent = agentContent.replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+)?/, '- **Version:** 0.4.0'); + fs.writeFileSync(agentPath, agentContent); + + // Delete identity files to simulate old installation + const identityDir = path.join(tmpDir, '.squad', 'identity'); + fs.rmSync(identityDir, { recursive: true, force: true }); + + // Run upgrade + const result = runSquad(['upgrade'], tmpDir); + assert.equal(result.exitCode, 0, `upgrade should succeed: ${result.stdout}`); + + // Check files were created - upgrade creates them because directory creation happens every time + const nowPath = path.join(tmpDir, '.squad', 'identity', 'now.md'); + const wisdomPath = path.join(tmpDir, '.squad', 'identity', 'wisdom.md'); + + // The directory is created, and files are scaffolded if missing + assert.ok(fs.existsSync(identityDir), 'upgrade should create identity directory'); + assert.ok(fs.existsSync(nowPath), 'upgrade should create missing now.md'); + assert.ok(fs.existsSync(wisdomPath), 'upgrade should create missing wisdom.md'); + }); +});