Skip to content

Route squad state through runtime tools#1158

Merged
bradygaster merged 7 commits into
bradygaster:devfrom
tamirdresher:squad/two-layer-runtime-state-api
May 25, 2026
Merged

Route squad state through runtime tools#1158
bradygaster merged 7 commits into
bradygaster:devfrom
tamirdresher:squad/two-layer-runtime-state-api

Conversation

@tamirdresher
Copy link
Copy Markdown
Collaborator

Closes #1157

Summary

  • add runtime-owned state.read/write/append/delete/list/health tools backed by the configured storage provider
  • fail closed when explicit git-native state backends cannot initialize
  • update squad prompts/templates to stop manual git notes / squad-state choreography
  • add regression coverage for git-native state routing and .squad key normalization

Validation

  • npx vitest run test/state-backend.test.ts --testNamePattern "ToolRegistry state tools|fails closed" --maxWorkers=1 --reporter=default
  • npx vitest run test/state/squad-state-wiring.test.ts --maxWorkers=1 --reporter=default
  • npm run lint
  • npm run build -w packages/squad-cli
  • git --no-pager diff --check

Add backend-owned state tools for mutable Squad state, fail closed
when explicit git-native backends are unavailable, and update agent
prompts/templates to avoid manual git notes or squad-state choreography.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 23, 2026 06:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves agent-facing mutable Squad state interactions behind a runtime-owned state.* tool boundary, so state persistence routes through the configured storage provider/state backend instead of prompt-level git/file choreography. It also tightens backend initialization behavior to “fail closed” when an explicitly configured git-native backend can’t be used.

Changes:

  • Added state.read/write/append/delete/list/health tools to ToolRegistry, backed by the injected StorageProvider (e.g., StateBackendStorageAdapter for git-native backends).
  • Updated state backend resolution to require a Git repo for git-native backends and to throw when an explicit backend can’t initialize (no silent fallback).
  • Updated squad agent templates to instruct using runtime state tools (and to stop manual git notes / squad-state steps) and added regression tests for routing + key normalization.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/squad-sdk/src/tools/index.ts Adds state.* tools and key normalization helpers; registers the new tools in ToolRegistry.
packages/squad-sdk/src/state-backend.ts Ensures git-native backends require a git repository and “fail closed” when explicitly configured but unavailable.
test/state-backend.test.ts Adds regression coverage for explicit-backend failure behavior and ToolRegistry→adapter routing behavior (including .squad key normalization edge case).
templates/squad.agent.md.template Updates directive-capture guidance to prefer runtime state tools and avoid manual git state choreography.
packages/squad-cli/templates/squad.agent.md.template Mirrors the template guidance update for the CLI template copy.
packages/squad-sdk/templates/squad.agent.md.template Mirrors the template guidance update for the SDK template copy.
.squad-templates/squad.agent.md Updates canonical squad agent template content for runtime-owned state tool usage.
.github/agents/squad.agent.md Mirrors the updated canonical agent guidance into the GitHub Agents location.

Comment on lines +236 to +244
function normalizeStateToolKey(key: string): string {
const normalized = key
.replace(/\\/g, '/')
.replace(/^\/+/, '')
.replace(/^\.squad(?:\/|$)/, '')
.replace(/\/+$/, '');
validateStateKey(normalized);
return normalized;
}
Comment on lines +679 to +776
const stateAppend = defineTool<StateAppendRequest>({
name: 'state.append',
description: 'Append to mutable Squad state through the configured state backend. Keys are relative to .squad/.',
parameters: {
type: 'object',
properties: {
key: { type: 'string', description: 'State key relative to .squad/' },
content: { type: 'string', description: 'Content to append' },
},
required: ['key', 'content'],
},
handler: async (args) => {
try {
const key = normalizeStateToolKey(args.key);
this.storage.appendSync(path.join(this.squadRoot, key), args.content);
return {
textResultForLlm: `State appended: ${key}`,
resultType: 'success',
toolTelemetry: { key },
};
} catch (error) {
return {
textResultForLlm: `Failed to append state: ${sanitizeErrorForLlm(error, this.squadRoot)}`,
resultType: 'failure',
error: String(error),
};
}
},
});

const stateDelete = defineTool<StateDeleteRequest>({
name: 'state.delete',
description: 'Delete mutable Squad state through the configured state backend. Keys are relative to .squad/.',
parameters: {
type: 'object',
properties: {
key: { type: 'string', description: 'State key relative to .squad/' },
},
required: ['key'],
},
handler: async (args) => {
try {
const key = normalizeStateToolKey(args.key);
this.storage.deleteSync(path.join(this.squadRoot, key));
return {
textResultForLlm: `State deleted: ${key}`,
resultType: 'success',
toolTelemetry: { key },
};
} catch (error) {
return {
textResultForLlm: `Failed to delete state: ${sanitizeErrorForLlm(error, this.squadRoot)}`,
resultType: 'failure',
error: String(error),
};
}
},
});

const stateList = defineTool<StateListRequest>({
name: 'state.list',
description: 'List mutable Squad state entries through the configured state backend. Directories are relative to .squad/.',
parameters: {
type: 'object',
properties: {
dir: { type: 'string', description: 'Directory relative to .squad/; omit for root' },
},
},
handler: async (args) => {
try {
const dir = normalizeStateToolDir(args.dir);
const entries = this.storage.listSync(path.join(this.squadRoot, dir));
return {
textResultForLlm: entries.length > 0 ? entries.join('\n') : '(empty)',
resultType: 'success',
toolTelemetry: { dir },
};
} catch (error) {
return {
textResultForLlm: `Failed to list state: ${sanitizeErrorForLlm(error, this.squadRoot)}`,
resultType: 'failure',
error: String(error),
};
}
},
});

const stateHealth = defineTool<Record<string, never>>({
name: 'state.health',
description: 'Report the active Squad state storage layer so agents can verify they are using runtime-owned state instead of manual git/file choreography.',
parameters: { type: 'object', properties: {} },
handler: async () => ({
textResultForLlm: `State backend storage: ${this.storage.constructor.name}`,
resultType: 'success',
toolTelemetry: { storage: this.storage.constructor.name },
}),
});

Comment thread packages/squad-sdk/src/tools/index.ts Outdated
Comment on lines +1143 to +1152
@@ -935,6 +1144,12 @@ export class ToolRegistry {
this.tools.set('squad_route', squadRoute);
this.tools.set('squad_decide', squadDecide);
this.tools.set('squad_memory', squadMemory);
this.tools.set('state.read', stateRead);
this.tools.set('state.write', stateWrite);
this.tools.set('state.append', stateAppend);
this.tools.set('state.delete', stateDelete);
this.tools.set('state.list', stateList);
this.tools.set('state.health', stateHealth);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should all squad tools be namespaced with a squad prefix?

Copilot and others added 3 commits May 23, 2026 15:14
Replace remaining manual mutable state prompt paths with runtime state tool instructions and add replay-derived coverage for the downloaded two-layer failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .github/agents/squad.agent.md Outdated
1. Capture the directive:
- **worktree/orphan backend:** Write it immediately to `.squad/decisions/inbox/copilot-directive-{timestamp}.md` using this format:
1. Capture the directive with the runtime state tools when available:
- Prefer `state_write` (MCP alias for runtime `state.write`) to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this try to be stricter? if the state tool is available, always use it?

Copilot and others added 3 commits May 25, 2026 14:58
Restrict mutable state tool writes to approved runtime state paths, refresh spawn/Scribe guidance, and fix CI gate regressions for help output, datetime templates, and package versions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bradygaster bradygaster merged commit 64def6e into bradygaster:dev May 25, 2026
6 checks passed
obit91 added a commit to obit91/squad that referenced this pull request May 26, 2026
…anges

Upstream PR bradygaster#1175 (merged after this branch was cut) updated
.squad-templates/scribe-charter.md:

- Step 5 renamed: 'Verify persistence through the runtime backend'
  -> 'Commit and verify persistence through the runtime backend'
  (capital V -> lowercase v in 'verify').
- Prohibition sentence on the same step changed from
  'Never commit, amend, reset, checkout, push notes, or switch
  branches to persist mutable squad state.'
  to 'Never amend, reset, checkout, push notes, or switch branches
  to persist mutable squad state. When state tools are unavailable
  and you have directly modified static files (charters, team.md,
  skills), commit those changes with git commit.'

The word 'commit' was intentionally REMOVED from the mutable-state
prohibition: Scribe is now allowed to commit STATIC files (charters,
team.md, skills) as a fallback when state tools are unavailable.
Amend / reset / checkout / push notes / switch branches remain
forbidden for mutable squad state.

Updates:
- 'persistence-verification step is present' and the ordering test now
  match 'verify persistence' case-insensitively so the rename is
  tolerated and a future re-rename is caught loudly rather than
  silently passing on a stale phrase.
- Renamed the 'forbids committing' assertion to 'forbids amending'
  and switched the first regex from 'Never commit' to 'Never amend'
  to reflect the new contract. Kept the 'push notes' and
  'switch branches' assertions unchanged.
- Updated the file header comment to cite bradygaster#1175 alongside bradygaster#1158.

Verified locally: all 16 tests in test/ci/datetime-template.test.ts
and test/ci/scribe-template.test.ts pass.

Related: bradygaster#1177

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two-layer state backend is bypassed by prompt-level manual git state choreography

4 participants