Route squad state through runtime tools#1158
Merged
bradygaster merged 7 commits intoMay 25, 2026
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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/healthtools toToolRegistry, backed by the injectedStorageProvider(e.g.,StateBackendStorageAdapterfor 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-statesteps) 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 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); | |||
There was a problem hiding this comment.
should all squad tools be namespaced with a squad prefix?
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>
bradygaster
approved these changes
May 24, 2026
serbrech
reviewed
May 24, 2026
| 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: |
There was a problem hiding this comment.
should this try to be stricter? if the state tool is available, always use it?
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>
This was referenced May 26, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1157
Summary
state.read/write/append/delete/list/healthtools backed by the configured storage providergit notes/squad-statechoreography.squadkey normalizationValidation
npx vitest run test/state-backend.test.ts --testNamePattern "ToolRegistry state tools|fails closed" --maxWorkers=1 --reporter=defaultnpx vitest run test/state/squad-state-wiring.test.ts --maxWorkers=1 --reporter=defaultnpm run lintnpm run build -w packages/squad-cligit --no-pager diff --check