Skip to content

Commit 0448c64

Browse files
authored
Merge branch 'dev' into squad/claude-live-evidence
2 parents c774b53 + 8c2c28b commit 0448c64

5 files changed

Lines changed: 59 additions & 8 deletions

File tree

‎.changeset/decide-where-stored.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bradygaster/squad-sdk": patch
3+
---
4+
5+
`squad_decide` says where a decision went on a git-branch backend (`orphan`, `two-layer`): the path is a key on the `squad-state` branch, not a file in the working tree.

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ Claude Code is the first host after GitHub Copilot where Squad uses the host's o
7878
- `.squad/squad.mjs` is a launcher Squad commits. It finds a Squad CLI on each machine: `SQUAD_CLI`, the project's `node_modules`, then `squad` on PATH. It skips a CLI older than the one that wrote it, and uses `npx` only with `SQUAD_ALLOW_NPX=1`. It shares its code with the plugin's launcher.
7979
- Claude hooks reach the launcher through `$CLAUDE_PROJECT_DIR`. Standalone bundle installs write `squad state-mcp`. A CLI run through `npx` keeps the exact-version `npx` command.
8080
- `squad doctor` flags machine-specific launch commands and a missing launcher. Run `squad upgrade` once to fix an existing repository.
81-
- **Squad's own tools no longer prompt in Claude Code.** In a demo dry run, a fresh `squad init --client claude` project hit 13 permission prompts while casting the team, and the coordinator ended up hand-writing charters and asking the user to run `squad cast` themselves. `squad init` and `squad upgrade` now add a scoped allowlist to `.claude/settings.json`: the `squad_state` tools the coordinator uses, by name (never the experimental backend's approval tools), and `squad cast`, `squad client sync` `squad lockout reject` and `squad lockout status` (the steps that release a lockout, `assign`, `submit`, `approve` and `override`, still ask), as `squad …` and, when `.mcp.json` runs Squad's own launcher, as `node .squad/squad.mjs …`. Nothing else is allowed, and `squad cast` now refuses to write through a symbolic link. Your own rules come first and are never touched; Squad records the rules it added in `.claude/.squad-permissions.json`, and rollback removes only those, and `"claudePermissions": "off"` in `.squad/config.json` turns it off. Claude Code applies project allow rules only after the folder is trusted (the trust prompt on your first interactive `claude` run there). `squad init` now says so, and if a cast is still denied the coordinator stops and asks instead of retrying variants or hand-writing the team.
81+
- **Squad's own tools no longer prompt in Claude Code.** In a demo dry run, a fresh `squad init --client claude` project hit 13 permission prompts while casting the team, and the coordinator ended up hand-writing charters and asking the user to run `squad cast` themselves. `squad init` and `squad upgrade` now add a scoped allowlist to `.claude/settings.json`: the `squad_state` tools the coordinator uses, by name (never the experimental backend's approval tools), and `squad cast`, `squad client sync`, `squad lockout reject` and `squad lockout status` (the steps that release a lockout, `assign`, `submit`, `approve` and `override`, still ask), as `squad …` and, when `.mcp.json` runs Squad's own launcher, as `node .squad/squad.mjs …`. Nothing else is allowed, and `squad cast` now refuses to write through a symbolic link. Your own rules come first and are never touched; Squad records the rules it added in `.claude/.squad-permissions.json`, and rollback removes only those, and `"claudePermissions": "off"` in `.squad/config.json` turns it off. Claude Code applies project allow rules only after the folder is trusted (the trust prompt on your first interactive `claude` run there). `squad init` now says so, and if a cast is still denied the coordinator stops and asks instead of retrying variants or hand-writing the team.
82+
- **`squad_decide` says where a git-branch backend stored the decision.** In a live run on the `orphan` backend, the coordinator told the user the decision was in `.squad/decisions/inbox/…`, but it was only on the `squad-state` branch. The tool result now says the path is a key on that branch, so the coordinator reports it correctly.
8283
- **The reviewer gate runs on its own and means it.** In the same dry run, the coordinator declared a bug fix done without the lead's review even though `routing.md` named the lead as the approval gate; when asked, the lead listed six known gaps against the issue and still approved; and a reviewer's own fix went unreviewed. The non-Copilot coordinators now send every code or test change to the reviewer before calling it done, unasked; the reviewer is never an author, judges against the issue and the team's recorded decisions (a known gap against either is a REJECT, not an advisory), and code a reviewer writes needs a different reviewer. The coordinator also names a recorded decision when it shapes the work ("Per the team decision of <date>: …").
8384
- **Coordinators wait for a human's sign-off.** When `team.md` or `routing.md` says a human must sign off on work (a release, tag, publish, merge or deploy), the Claude, Kiro, OpenCode, Cursor, Codex and Antigravity coordinators no longer treat the request itself as the sign-off. They name the rule and the human, ask whether you are that human or have their sign-off, go ahead only on an explicit yes, and record it in the decisions inbox. In a non-interactive run they stop at the gate. Seen in a live test: asked to "cut the 1.0.0 release", the Claude coordinator used to bump, tag and run `npm publish` itself.
8485
- `squad_state` works when the team folder is reached through a symlink, such as macOS `/tmp` (really `/private/tmp`) or a linked team root. Squad stores the folder's resolved path but was given paths in the original spelling, so it refused reads and writes there with "Path traversal blocked". Paths that really leave the team folder, including through a symlink inside it, are still refused.

‎docs/specification/adr-003-host-neutral-canonical-coordinator.md‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# ADR 003: Host-Neutral Canonical Coordinator in the Workspace
22

3-
**Status:** Proposed
3+
**Status:** Accepted (option C), 2026-11-13
44

55
**Tracking:** [#2438](https://github.com/github/squad/issues/2438) (Phase 7)
66

77
**Affects:** `squad-client-harness-adapters/v0.2` §4–§5 (projection contract
88
and canonical paths) and §13 (upgrade and rollback)
99

10-
This ADR records a decision to be made. It does not change any code. Merging it
11-
with status Proposed does not approve it.
10+
The maintainers chose option C. Option B stays an opt-in experiment
11+
([#2488](https://github.com/github/squad/issues/2488)) and is not part of 2.0.
1212

1313
## Context
1414

@@ -185,12 +185,28 @@ coordinator behaves correctly, so C must be shown with behavioral scenarios run
185185
with the Copilot file absent. C is tracked in
186186
[#2487](https://github.com/github/squad/issues/2487) and the B spike in
187187
[#2488](https://github.com/github/squad/issues/2488). This records the
188-
discussion; it does not accept this ADR.
188+
discussion; the decision is recorded below.
189189

190190
## Decision
191191

192-
Not yet made. Implementation, if approved, is a separate PR with its own
193-
projection version bump and CHANGELOG entry.
192+
**Option C**, agreed by the maintainers on 2026-11-13.
193+
194+
- `.github/agents/squad.agent.md` stays the canonical coordinator, and it is
195+
defined only for Copilot. Copilot's output doesn't change.
196+
- Every other host's digest is complete on its own and never points at
197+
Copilot's file. The digest parity tests keep the hosts in step. This shipped
198+
in [#2489](https://github.com/github/squad/pull/2489).
199+
- Behavioral scenarios ran live on Claude Code with Copilot's file deleted, and
200+
passed: routing, human sign-off, reviewer rejection, restart continuity,
201+
customization precedence, monorepo subfolder, backend outage and the
202+
satellite repository. Azure DevOps is deferred. The evidence is in
203+
[#2487](https://github.com/github/squad/issues/2487) and the host feature
204+
matrix.
205+
- Option B (a host-neutral canonical file that Copilot's is generated from) is
206+
not adopted. It stays a bounded, opt-in spike in
207+
[#2488](https://github.com/github/squad/issues/2488). Revisit it if Copilot's
208+
file needs host-specific content, or if remote teams need the coordinator
209+
served through `squad_state`.
194210

195211
## Related artifacts
196212

‎packages/squad-sdk/src/tools/index.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ export class ToolRegistry {
392392
private fanOutDepsGetter?: () => FanOutDependencies | undefined;
393393
private memoryStore: LocalMemoryStore;
394394
private backendName: string;
395+
/**
396+
* Where a write landed, for tool results: with a git-branch backend the path is a key on the
397+
* `squad-state` branch, not a file in the working tree, and a coordinator reporting the path to
398+
* the user said otherwise (live scenario, 2026-11-13).
399+
*/
400+
private storedWhere(): string {
401+
return this.backendName === 'orphan' || this.backendName === 'two-layer'
402+
? ` That path is a key on the \`squad-state\` git branch (${this.backendName} backend): it is stored only there, and there is no file in the working tree.`
403+
: '';
404+
}
395405
private runtimeVersion: string;
396406
private cliVersion: string;
397407
private launchMode: string;
@@ -654,7 +664,7 @@ export class ToolRegistry {
654664
this.storage.writeSync(filename, content);
655665

656666
return {
657-
textResultForLlm: `Decision written: ${inboxRel}/${authorSlug}-${slug}.md (ID: ${decisionId})`,
667+
textResultForLlm: `Decision written: ${inboxRel}/${authorSlug}-${slug}.md (ID: ${decisionId})${this.storedWhere()}`,
658668
resultType: 'success',
659669
toolTelemetry: { decisionId, filename, slug, inbox: inboxRel },
660670
};

‎test/tools.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,25 @@ describe('squad_decide handler', () => {
577577

578578
// #1256: on-behalf-of authors with spaces/parens/etc must be accepted;
579579
// filename must be slugified; author > 200 chars must be rejected.
580+
describe('squad_decide says where a git-branch backend stored the decision (live scenario, 2026-11-13)', () => {
581+
const testRoot = path.join('.', '.test-squad-decide-where-' + randomUUID());
582+
afterEach(() => fs.rmSync(testRoot, { recursive: true, force: true }));
583+
const decide = (registry: ToolRegistry) =>
584+
registry.getTool('squad_decide')!.handler(
585+
{ author: 'fenster', summary: 'Use ESM', body: 'Named exports only.' } as DecisionRecord,
586+
{ sessionId: 's', toolCallId: 't', toolName: 'squad_decide', arguments: {} },
587+
);
588+
589+
it('names the squad-state branch for orphan and two-layer, and adds nothing for local', async () => {
590+
for (const backendName of ['orphan', 'two-layer']) {
591+
const r = await decide(new ToolRegistry(testRoot, undefined, undefined, undefined, undefined, undefined, { backendName }));
592+
expect(r.textResultForLlm).toContain(`That path is a key on the \`squad-state\` git branch (${backendName} backend): it is stored only there, and there is no file in the working tree.`);
593+
}
594+
const local = await decide(new ToolRegistry(testRoot));
595+
expect(local.textResultForLlm).not.toMatch(/squad-state/);
596+
});
597+
});
598+
580599
describe('squad_decide author validation (Issue #1256)', () => {
581600
let registry: ToolRegistry;
582601
let testRoot: string;

0 commit comments

Comments
 (0)