Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/memory-tools-mcp-exposure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bradygaster/squad-cli": patch
---

Expose memory.* tools (classify, write, search, promote, delete, audit) through the squad_state MCP server so agents can discover and call them. Also pin squad_state to user-level ~/.copilot/mcp-config.json for external `copilot -p` mode compatibility.
18 changes: 10 additions & 8 deletions .github/agents/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,16 @@ The `name` parameter generates the human-readable agent ID shown in the tasks pa

**When you detect a directive:**

1. Capture the directive with the runtime state tools when available:
- Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format:
1. Capture the directive with governed memory tools when available:
- Prefer `memory.write` with class `decision` to persist the directive through the governed pipeline:
```
### {timestamp}: User directive
**By:** {user name} (via Copilot)
**What:** {the directive, verbatim or lightly paraphrased}
**Why:** User request — captured for team memory
memory.write({
class: "decision",
key: "copilot-directive-{timestamp}",
content: "### {timestamp}: User directive\n**By:** {user name} (via Copilot)\n**What:** {the directive, verbatim or lightly paraphrased}\n**Why:** User request — captured for team memory"
})
Comment on lines +262 to +266
```
- If `memory.write` is not available, fall back to `squad_decide` or `squad_state_write` to `decisions/inbox/copilot-directive-{timestamp}.md`.
- Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence.
2. Acknowledge briefly: `"📌 Captured. {one-line summary of the directive}."`
3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed.
Expand Down Expand Up @@ -403,7 +405,7 @@ prompt: |
TARGET FILE(S): {exact file path(s)}

Do the work. Keep it focused.
If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.
If you made a meaningful decision, persist it with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.

⚠️ OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL.
⚠️ RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output.
Expand Down Expand Up @@ -528,7 +530,7 @@ When the user gives any task, the Coordinator MUST:
To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts:

**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead:
- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- Agents record decisions with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits.
- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox
- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot)
Expand Down
18 changes: 10 additions & 8 deletions .squad-templates/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,16 @@ The `name` parameter generates the human-readable agent ID shown in the tasks pa

**When you detect a directive:**

1. Capture the directive with the runtime state tools when available:
- Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format:
1. Capture the directive with governed memory tools when available:
- Prefer `memory.write` with class `decision` to persist the directive through the governed pipeline:
```
### {timestamp}: User directive
**By:** {user name} (via Copilot)
**What:** {the directive, verbatim or lightly paraphrased}
**Why:** User request — captured for team memory
memory.write({
class: "decision",
key: "copilot-directive-{timestamp}",
content: "### {timestamp}: User directive\n**By:** {user name} (via Copilot)\n**What:** {the directive, verbatim or lightly paraphrased}\n**Why:** User request — captured for team memory"
})
Comment on lines +262 to +266
```
- If `memory.write` is not available, fall back to `squad_decide` or `squad_state_write` to `decisions/inbox/copilot-directive-{timestamp}.md`.
- Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence.
2. Acknowledge briefly: `"📌 Captured. {one-line summary of the directive}."`
3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed.
Expand Down Expand Up @@ -403,7 +405,7 @@ prompt: |
TARGET FILE(S): {exact file path(s)}

Do the work. Keep it focused.
If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.
If you made a meaningful decision, persist it with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.

⚠️ OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL.
⚠️ RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output.
Expand Down Expand Up @@ -528,7 +530,7 @@ When the user gives any task, the Coordinator MUST:
To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts:

**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead:
- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- Agents record decisions with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits.
- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox
- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot)
Expand Down
6 changes: 6 additions & 0 deletions packages/squad-cli/src/cli/commands/state-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ const MCP_TOOL_ALIASES: Record<string, string> = {
squad_state_delete: 'squad_state_delete',
squad_state_list: 'squad_state_list',
squad_state_health: 'squad_state_health',
'memory.classify': 'memory.classify',
'memory.write': 'memory.write',
'memory.search': 'memory.search',
'memory.promote': 'memory.promote',
'memory.delete': 'memory.delete',
'memory.audit': 'memory.audit',
Comment on lines +35 to +40
};

function parseObject(value: unknown): Record<string, unknown> {
Expand Down
7 changes: 6 additions & 1 deletion packages/squad-cli/src/cli/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { installGitHooks } from '../commands/install-hooks.js';
import { liftInitMutableStateOntoOrphan } from '../commands/migrate-backend.js';
import { resolveSquadStateMcpSpec } from './mcp-spec.js';
import { describeMcpSpec } from './upgrade.js';
import { ensureSquadStateMcpInRoot, tombstoneStaleSquadStateInProjectMcp } from './mcp-root.js';
import { ensureSquadStateMcpInRoot, ensureSquadStateMcpInUserConfig, tombstoneStaleSquadStateInProjectMcp } from './mcp-root.js';

const storage = new FSStorageProvider();

Expand Down Expand Up @@ -393,6 +393,11 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi
if (rootResult.written) {
success(`installed squad_state MCP server to .mcp.json (${describeMcpSpec(mcpSpec)}) — Copilot CLI will auto-load on next invocation`);
}
// Also pin to user-level config for external `copilot -p` compatibility
const userResult = ensureSquadStateMcpInUserConfig(dest, mcpSpec);
if (userResult.written) {
success(`pinned squad_state to ~/.copilot/mcp-config.json for \`copilot -p\` mode compatibility`);
}
Comment on lines +396 to +400
const tomb = tombstoneStaleSquadStateInProjectMcp(dest);
if (tomb.removed) {
success(`removed stale squad_state from ${tomb.path} (now lives in .mcp.json)`);
Expand Down
83 changes: 83 additions & 0 deletions packages/squad-cli/src/cli/core/mcp-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,86 @@ export function tombstoneStaleSquadStateInProjectMcp(dest: string): TombstoneRes
storage.writeSync(cfgPath, JSON.stringify(config, null, 2) + '\n');
return { removed: true, path: cfgPath };
}

/**
* Pin `squad_state` to user-level `~/.copilot/mcp-config.json` so that
* external `copilot -p` invocations (which skip workspace `.mcp.json` due
* to the folder-trust security gate) still have access to squad_state tools.
*
* Unlike the repo-root writer, this writes a SINGLE entry keyed by the
* project's absolute path hash to avoid collisions when multiple Squad
* projects coexist. The key format is `squad_state_<shortHash>`.
*
* Best-effort: silently no-ops on parse failure to avoid corrupting the
* user's hand-edited config.
*/
export function ensureSquadStateMcpInUserConfig(
dest: string,
spec: SquadStateMcpSpec,
): EnsureRootResult {
const homedir = process.env.HOME || process.env.USERPROFILE || '';
if (!homedir) return { written: false, key: '', path: '' };

const cfgPath = path.join(homedir, '.copilot', 'mcp-config.json');
// Stable short hash of the project path for the key suffix
const hash = simpleHash(path.resolve(dest));
const key = `squad_state_${hash}`;

let parsed: McpConfigShape;
if (storage.existsSync(cfgPath)) {
const raw = storage.readSync(cfgPath) ?? '{}';
try {
const obj = JSON.parse(raw) as unknown;
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
return { written: false, key, path: cfgPath };
}
parsed = obj as McpConfigShape;
} catch {
return { written: false, key, path: cfgPath };
}
} else {
parsed = {};
}

if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
parsed.mcpServers = {};
}

const existing = parsed.mcpServers[key];
const desired: McpServerEntry = {
command: spec.command,
args: [...spec.args],
env: {},
tools: ['*'],
};

if (
existing &&
existing.command === desired.command &&
Array.isArray(existing.args) &&
existing.args.length === desired.args!.length &&
existing.args.every((a, i) => a === desired.args![i])
) {
return { written: false, key, path: cfgPath };
}
Comment on lines +218 to +226

parsed.mcpServers[key] = desired;

// Ensure parent directory exists
const cfgDir = path.dirname(cfgPath);
if (!storage.existsSync(cfgDir)) {
storage.mkdirSync(cfgDir, { recursive: true });
}

storage.writeSync(cfgPath, JSON.stringify(parsed, null, 2) + '\n');
return { written: true, key, path: cfgPath };
}

/** Simple deterministic 8-char hex hash for path-based keys. */
function simpleHash(input: string): string {
let h = 0;
for (let i = 0; i < input.length; i++) {
h = ((h << 5) - h + input.charCodeAt(i)) | 0;
}
return (h >>> 0).toString(16).padStart(8, '0');
}
11 changes: 10 additions & 1 deletion packages/squad-cli/src/cli/core/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { scrubEmails } from './email-scrub.js';
import { getPackageVersion, stampVersion, readInstalledVersion } from './version.js';
import { resolveSquadStateMcpSpec, type SquadStateMcpSpec } from './mcp-spec.js';
export { resolveSquadStateMcpSpec } from './mcp-spec.js';
import { ensureSquadStateMcpInRoot, tombstoneStaleSquadStateInProjectMcp } from './mcp-root.js';
import { ensureSquadStateMcpInRoot, ensureSquadStateMcpInUserConfig, tombstoneStaleSquadStateInProjectMcp } from './mcp-root.js';

const storage = new FSStorageProvider();

Expand Down Expand Up @@ -710,6 +710,15 @@ async function runEnsureChecks(dest: string, templatesDir: string, filesUpdated:
} catch (err) {
warn(`Could not write .mcp.json: ${err instanceof Error ? err.message : err}`);
}
// Also pin to user-level config for external `copilot -p` compatibility
try {
const userResult = ensureSquadStateMcpInUserConfig(dest, pinnedSpec);
if (userResult.written) {
success(`pinned squad_state to ~/.copilot/mcp-config.json for \`copilot -p\` mode compatibility`);
}
Comment on lines +713 to +718
} catch {
// best-effort: user-level config write failure does not block upgrade
}
const tomb = tombstoneStaleSquadStateInProjectMcp(dest);
if (tomb.removed) {
success(`removed stale squad_state from ${tomb.path} (now lives in .mcp.json)`);
Expand Down
18 changes: 10 additions & 8 deletions packages/squad-cli/templates/squad.agent.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,16 @@ The `name` parameter generates the human-readable agent ID shown in the tasks pa

**When you detect a directive:**

1. Capture the directive with the runtime state tools when available:
- Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format:
1. Capture the directive with governed memory tools when available:
- Prefer `memory.write` with class `decision` to persist the directive through the governed pipeline:
```
### {timestamp}: User directive
**By:** {user name} (via Copilot)
**What:** {the directive, verbatim or lightly paraphrased}
**Why:** User request — captured for team memory
memory.write({
class: "decision",
key: "copilot-directive-{timestamp}",
content: "### {timestamp}: User directive\n**By:** {user name} (via Copilot)\n**What:** {the directive, verbatim or lightly paraphrased}\n**Why:** User request — captured for team memory"
})
Comment on lines +262 to +266
```
- If `memory.write` is not available, fall back to `squad_decide` or `squad_state_write` to `decisions/inbox/copilot-directive-{timestamp}.md`.
- Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence.
2. Acknowledge briefly: `"📌 Captured. {one-line summary of the directive}."`
3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed.
Expand Down Expand Up @@ -403,7 +405,7 @@ prompt: |
TARGET FILE(S): {exact file path(s)}

Do the work. Keep it focused.
If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.
If you made a meaningful decision, persist it with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.

⚠️ OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL.
⚠️ RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output.
Expand Down Expand Up @@ -528,7 +530,7 @@ When the user gives any task, the Coordinator MUST:
To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts:

**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead:
- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- Agents record decisions with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits.
- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox
- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot)
Expand Down
18 changes: 10 additions & 8 deletions packages/squad-sdk/templates/squad.agent.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,16 @@ The `name` parameter generates the human-readable agent ID shown in the tasks pa

**When you detect a directive:**

1. Capture the directive with the runtime state tools when available:
- Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format:
1. Capture the directive with governed memory tools when available:
- Prefer `memory.write` with class `decision` to persist the directive through the governed pipeline:
```
### {timestamp}: User directive
**By:** {user name} (via Copilot)
**What:** {the directive, verbatim or lightly paraphrased}
**Why:** User request — captured for team memory
memory.write({
class: "decision",
key: "copilot-directive-{timestamp}",
content: "### {timestamp}: User directive\n**By:** {user name} (via Copilot)\n**What:** {the directive, verbatim or lightly paraphrased}\n**Why:** User request — captured for team memory"
})
Comment on lines +262 to +266
```
- If `memory.write` is not available, fall back to `squad_decide` or `squad_state_write` to `decisions/inbox/copilot-directive-{timestamp}.md`.
- Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence.
2. Acknowledge briefly: `"📌 Captured. {one-line summary of the directive}."`
3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed.
Expand Down Expand Up @@ -403,7 +405,7 @@ prompt: |
TARGET FILE(S): {exact file path(s)}

Do the work. Keep it focused.
If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.
If you made a meaningful decision, persist it with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand.

⚠️ OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL.
⚠️ RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output.
Expand Down Expand Up @@ -528,7 +530,7 @@ When the user gives any task, the Coordinator MUST:
To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts:

**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead:
- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- Agents record decisions with `memory.write` (class: `decision`) when available, or fall back to `squad_decide` / `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`.
- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits.
- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox
- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot)
Expand Down
Loading
Loading