From 5b1cdd21abc43cc06f813fd52aa7a91bdcba15bc Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 3 Sep 2026 12:00:03 +0300 Subject: [PATCH] fix: refresh stale squad.agent.md greeting version on re-init Re-running squad init against an already-initialized project could leave the squad.agent.md first-response greeting (backtick-quoted ` Squad v... `) stamped with an old version even though the HTML comment marker and Identity Version: line were correctly refreshed. Root cause: stampVersion (squad-cli) and stampVersionInContent (squad-sdk) each replace three version locations, but the third regex only matched the unresolved ` Squad v{version} ` placeholder. Once a real version had been stamped once, the placeholder was gone, so later re-stamps could no longer touch the greeting literal. Fix: broaden the greeting regex in both functions to also match an already-resolved ` Squad vX.Y.Z ` literal, so all three locations stay idempotently in sync on every re-init or upgrade. Adds regression coverage in test/cli/init.test.ts: a direct stampVersion unit test, and a full re-init integration test that seeds a stale resolved version in all three locations and asserts init.ts's existing-file re-stamp brings them back in sync. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b73147c1-252d-473c-be2b-df22d9faf163 --- .../fix-reinit-stale-version-greeting.md | 6 ++ packages/squad-cli/src/cli/core/version.ts | 6 +- packages/squad-sdk/src/config/init.ts | 6 +- test/cli/init.test.ts | 62 ++++++++++++++++++- 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-reinit-stale-version-greeting.md diff --git a/.changeset/fix-reinit-stale-version-greeting.md b/.changeset/fix-reinit-stale-version-greeting.md new file mode 100644 index 000000000..fe38cdf96 --- /dev/null +++ b/.changeset/fix-reinit-stale-version-greeting.md @@ -0,0 +1,6 @@ +--- +"@bradygaster/squad-cli": patch +"@bradygaster/squad-sdk": patch +--- + +Fix `squad init` re-run on an existing project leaving the `squad.agent.md` first-response greeting (`` `Squad v...` ``) stamped with a stale version even though the HTML comment marker and Identity `Version:` line were correctly refreshed. The greeting regex previously only matched the unresolved `{version}` placeholder, so once a real version had been stamped once, later re-stamps could no longer update it. Both `stampVersion` (squad-cli) and `stampVersionInContent` (squad-sdk) now also match an already-resolved `` `Squad vX.Y.Z` `` literal, making all three version locations idempotently updatable on every re-init or upgrade. diff --git a/packages/squad-cli/src/cli/core/version.ts b/packages/squad-cli/src/cli/core/version.ts index cfe547d1b..65465eebb 100644 --- a/packages/squad-cli/src/cli/core/version.ts +++ b/packages/squad-cli/src/cli/core/version.ts @@ -38,8 +38,10 @@ export function stampVersion(filePath: string, version: string): void { content = content.replace(//m, ``); // Replace version in the Identity section's Version line content = content.replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/m, `- **Version:** ${version}`); - // Replace {version} placeholder in the greeting instruction so it's unambiguous - content = content.replace(/`Squad v\{version\}`/g, `\`Squad v${version}\``); + // Replace the greeting instruction's version literal so it's unambiguous. + // Matches both the unresolved `{version}` placeholder and an already-resolved + // semver (e.g. from a prior stamp) so re-running this idempotently refreshes it. + content = content.replace(/`Squad v[^`]*`/g, `\`Squad v${version}\``); storage.writeSync(filePath, content); } diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 123b26b6e..96676618f 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -631,9 +631,11 @@ function stampVersionInContent(content: string, version: string): string { /- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/m, `- **Version:** ${version}` ); - // Greeting placeholder: `Squad v{version}` + // Greeting placeholder: `Squad v{version}` — also matches an already-resolved + // semver (e.g. `Squad v0.11.0`) from a prior stamp so this is idempotent when + // re-run against a file that already went through stampVersionInContent. content = content.replace( - /`Squad v\{version\}`/g, + /`Squad v[^`]*`/g, `\`Squad v${version}\`` ); return content; diff --git a/test/cli/init.test.ts b/test/cli/init.test.ts index 667b90e0b..ce239781c 100644 --- a/test/cli/init.test.ts +++ b/test/cli/init.test.ts @@ -4,13 +4,13 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdir, rm, readdir, readFile } from 'fs/promises'; +import { mkdir, rm, readdir, readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { tmpdir } from 'os'; import { randomBytes } from 'crypto'; import { runInit } from '@bradygaster/squad-cli/core/init'; -import { getPackageVersion } from '@bradygaster/squad-cli/core/version'; +import { getPackageVersion, stampVersion } from '@bradygaster/squad-cli/core/version'; const TEST_ROOT = join(tmpdir(), `.test-cli-init-${randomBytes(4).toString('hex')}`); const TEST_HOME = join(tmpdir(), `.test-cli-init-home-${randomBytes(4).toString('hex')}`); @@ -67,6 +67,64 @@ describe('CLI: init command', () => { expect(content).toContain(`Squad v${currentVersion}`); }); + it('stampVersion should refresh an already-resolved greeting literal, not just the {version} placeholder (regression)', async () => { + // The greeting regex used to only match the unresolved `Squad v{version}` + // placeholder, so a file that already had a resolved (stale) literal from a + // prior stamp stayed stuck on the old version forever. + const agentPath = join(TEST_ROOT, 'squad.agent.md'); + const staleVersion = '0.9.0'; + const currentVersion = getPackageVersion(); + const before = [ + ``, + '', + `- **Version:** ${staleVersion}. Include it as \`Squad v${staleVersion}\` in your first response.`, + ].join('\n'); + await writeFile(agentPath, before, 'utf-8'); + + stampVersion(agentPath, currentVersion); + const after = await readFile(agentPath, 'utf-8'); + + expect(after).toContain(``); + expect(after).toContain(`- **Version:** ${currentVersion}`); + expect(after).toContain(`\`Squad v${currentVersion}\``); + expect(after).not.toContain(staleVersion); + }); + + it('re-running init against an existing project should refresh a stale resolved version everywhere, including the greeting literal (re-init regression)', async () => { + // First init creates squad.agent.md with the current version stamped + // everywhere. Simulate what a real "installed a while ago" repo looks + // like: all three version locations still show an older resolved + // version (not the unresolved {version} placeholder). + await runInit(TEST_ROOT); + + const agentPath = join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'); + const currentVersion = getPackageVersion(); + const staleVersion = '0.9.0'; + const escapedCurrent = currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + let content = await readFile(agentPath, 'utf-8'); + content = content + .replace(//, ``) + .replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/, `- **Version:** ${staleVersion}`) + .replace(new RegExp('`Squad v' + escapedCurrent + '`'), `\`Squad v${staleVersion}\``); + await writeFile(agentPath, content, 'utf-8'); + + // Sanity check: the stale literal is actually present before rerunning init. + const beforeRerun = await readFile(agentPath, 'utf-8'); + expect(beforeRerun).toContain(`\`Squad v${staleVersion}\``); + + // Re-init the existing project. squad.agent.md already exists, so init's + // template-copy step is skipped (skipExisting=true) — only the trailing + // "ensure version is fully stamped" step in runInit touches this file. + await runInit(TEST_ROOT); + + const afterRerun = await readFile(agentPath, 'utf-8'); + expect(afterRerun).toContain(``); + expect(afterRerun).toContain(`- **Version:** ${currentVersion}`); + expect(afterRerun).toContain(`\`Squad v${currentVersion}\``); + expect(afterRerun).not.toContain(staleVersion); + }); + it('should create .squad/ directory structure', async () => { await runInit(TEST_ROOT);