diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 566600186..655641842 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -3,14 +3,14 @@ name: Squad description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." --- - + You are **Squad (Coordinator)** — the orchestrator for this project's AI team. ### Coordinator Identity - **Name:** Squad (Coordinator) -- **Version:** 0.9.1-build.3 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.9.1-build.3` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Version:** 0.0.0-source (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v{version}` in your first response of each session (e.g., in the acknowledgment or greeting). - **Role:** Agent orchestration, handoff enforcement, reviewer gating - **Inputs:** User request, repository state, `.squad/decisions.md` - **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) @@ -875,13 +875,15 @@ prompt: | SPAWN MANIFEST: {spawn_manifest} Tasks (in order): - 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. - 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. - 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. - 4. CROSS-AGENT: Append team updates to affected agents' history.md. - 5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md. - 6. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. - 7. HISTORY SUMMARIZATION: If any history.md >12KB, summarize old entries to ## Core Context. + 0. PRE-CHECK: Stat decisions.md size and count inbox/ files. Record measurements. + 1. DECISIONS ARCHIVE [HARD GATE]: If decisions.md >= 20480 bytes, archive entries older than 30 days NOW. If >= 51200 bytes, archive entries older than 7 days. Do not skip this step. + 2. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. + 3. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. + 4. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. + 5. CROSS-AGENT: Append team updates to affected agents' history.md. + 6. HISTORY SUMMARIZATION [HARD GATE]: If any history.md >= 15360 bytes (15KB), summarize now. + 7. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. + 8. HEALTH REPORT: Log decisions.md before/after size, inbox count processed, history files summarized. Never speak to user. ⚠️ End with plain text summary after all tool calls. ``` diff --git a/.squad-templates/scribe-charter.md b/.squad-templates/scribe-charter.md index 9082faa45..eae443a64 100644 --- a/.squad-templates/scribe-charter.md +++ b/.squad-templates/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index 47458fcdf..655641842 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -875,13 +875,15 @@ prompt: | SPAWN MANIFEST: {spawn_manifest} Tasks (in order): - 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. - 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. - 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. - 4. CROSS-AGENT: Append team updates to affected agents' history.md. - 5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md. - 6. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. - 7. HISTORY SUMMARIZATION: If any history.md >12KB, summarize old entries to ## Core Context. + 0. PRE-CHECK: Stat decisions.md size and count inbox/ files. Record measurements. + 1. DECISIONS ARCHIVE [HARD GATE]: If decisions.md >= 20480 bytes, archive entries older than 30 days NOW. If >= 51200 bytes, archive entries older than 7 days. Do not skip this step. + 2. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. + 3. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. + 4. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. + 5. CROSS-AGENT: Append team updates to affected agents' history.md. + 6. HISTORY SUMMARIZATION [HARD GATE]: If any history.md >= 15360 bytes (15KB), summarize now. + 7. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. + 8. HEALTH REPORT: Log decisions.md before/after size, inbox count processed, history files summarized. Never speak to user. ⚠️ End with plain text summary after all tool calls. ``` diff --git a/.squad/agents/scribe/charter.md b/.squad/agents/scribe/charter.md index 0e45c24a6..1800b3127 100644 --- a/.squad/agents/scribe/charter.md +++ b/.squad/agents/scribe/charter.md @@ -15,10 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another - -## How I Work - -**Worktree awareness:** Use `TEAM ROOT` from spawn prompt; fallback: `git rev-parse --show-toplevel`. +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs After substantial work: 1. Log session to `.squad/log/{timestamp}-{topic}.md` (who, what, outcomes) diff --git a/.squad/scribe-charter.md b/.squad/scribe-charter.md index 35e333ea4..0729b7775 100644 --- a/.squad/scribe-charter.md +++ b/.squad/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/.squad/templates/scribe-charter.md b/.squad/templates/scribe-charter.md index 9082faa45..eae443a64 100644 --- a/.squad/templates/scribe-charter.md +++ b/.squad/templates/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/packages/squad-cli/templates/scribe-charter.md b/packages/squad-cli/templates/scribe-charter.md index 9082faa45..eae443a64 100644 --- a/packages/squad-cli/templates/scribe-charter.md +++ b/packages/squad-cli/templates/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/packages/squad-sdk/templates/scribe-charter.md b/packages/squad-sdk/templates/scribe-charter.md index 9082faa45..eae443a64 100644 --- a/packages/squad-sdk/templates/scribe-charter.md +++ b/packages/squad-sdk/templates/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/templates/scribe-charter.md b/templates/scribe-charter.md index 9082faa45..eae443a64 100644 --- a/templates/scribe-charter.md +++ b/templates/scribe-charter.md @@ -15,6 +15,10 @@ - `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) - `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) - Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs ## How I Work diff --git a/test/ci/scribe-template.test.ts b/test/ci/scribe-template.test.ts new file mode 100644 index 000000000..63037bc06 --- /dev/null +++ b/test/ci/scribe-template.test.ts @@ -0,0 +1,88 @@ +/** + * CI tests for the Scribe spawn template in squad.agent.md. + * + * Verifies that: + * - DECISIONS ARCHIVE runs BEFORE DECISION INBOX (archival first, while context is fresh) + * - HARD GATE label is present on the archive task (haiku cannot claim discretion) + * - Exact byte threshold 20480 is used (not the fuzzy ~20KB) + * - PRE-CHECK is task 0 (measure before acting) + * - HEALTH REPORT is the final task (observe after) + * - GIT COMMIT precedes HEALTH REPORT + * + * Canonical source: .squad-templates/squad.agent.md + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, '../..'); + +function readTemplate(): string { + return readFileSync(resolve(ROOT, '.squad-templates/squad.agent.md'), 'utf-8'); +} + +/** + * Extract the Scribe task block from the template. + * Returns the substring from "Tasks (in order):" up to (not including) "Never speak to user." + */ +function extractScribeTaskBlock(content: string): string { + const start = content.indexOf('Tasks (in order):'); + const end = content.indexOf('Never speak to user.', start); + if (start === -1 || end === -1) { + throw new Error('Could not locate Scribe task block in template'); + } + return content.slice(start, end); +} + +describe('Scribe spawn template — HARD GATE enforcement', () => { + const content = readTemplate(); + const taskBlock = extractScribeTaskBlock(content); + + it('DECISIONS ARCHIVE appears before DECISION INBOX in the task list', () => { + const archiveIndex = taskBlock.indexOf('DECISIONS ARCHIVE'); + const inboxIndex = taskBlock.indexOf('DECISION INBOX'); + expect(archiveIndex, 'DECISIONS ARCHIVE not found in task block').toBeGreaterThan(-1); + expect(inboxIndex, 'DECISION INBOX not found in task block').toBeGreaterThan(-1); + expect(archiveIndex, 'DECISIONS ARCHIVE must come before DECISION INBOX').toBeLessThan(inboxIndex); + }); + + it('DECISIONS ARCHIVE task carries the [HARD GATE] label', () => { + const archiveLine = taskBlock + .split('\n') + .find(line => line.includes('DECISIONS ARCHIVE')); + expect(archiveLine, 'DECISIONS ARCHIVE line not found').toBeDefined(); + expect(archiveLine, '[HARD GATE] label missing on DECISIONS ARCHIVE').toContain('[HARD GATE]'); + }); + + it('exact byte threshold 20480 is used — not the fuzzy ~20KB', () => { + expect(taskBlock, '20480 byte threshold not found').toContain('20480'); + expect(taskBlock, 'fuzzy ~20KB must be replaced with exact byte count').not.toMatch(/~20KB/); + }); + + it('PRE-CHECK is task 0 (the first numbered task)', () => { + const numberedLines = taskBlock + .split('\n') + .filter(l => l.trim().match(/^\d+\./)); + expect(numberedLines.length, 'No numbered tasks found').toBeGreaterThan(0); + expect(numberedLines[0], 'First task must be PRE-CHECK').toContain('PRE-CHECK'); + }); + + it('HEALTH REPORT is the final task in the list', () => { + const numberedLines = taskBlock + .split('\n') + .filter(l => l.trim().match(/^\d+\./)); + expect(numberedLines.length, 'No numbered tasks found').toBeGreaterThan(0); + expect(numberedLines[numberedLines.length - 1], 'Last task must be HEALTH REPORT').toContain('HEALTH REPORT'); + }); + + it('GIT COMMIT appears before HEALTH REPORT', () => { + const commitIndex = taskBlock.indexOf('GIT COMMIT'); + const healthIndex = taskBlock.indexOf('HEALTH REPORT'); + expect(commitIndex, 'GIT COMMIT not found').toBeGreaterThan(-1); + expect(healthIndex, 'HEALTH REPORT not found').toBeGreaterThan(-1); + expect(commitIndex, 'GIT COMMIT must precede HEALTH REPORT').toBeLessThan(healthIndex); + }); +});