-
Notifications
You must be signed in to change notification settings - Fork 304
feat(sdk): git-notes + orphan-branch state backends (#807) #810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --- | ||
| "@bradygaster/squad-sdk": minor | ||
| "@bradygaster/squad-cli": minor | ||
| --- | ||
|
|
||
| feat(sdk): git-notes + orphan-branch state backends for .squad/ | ||
|
|
||
| Adds two git-native state storage backends as alternatives to the worktree | ||
| and external directory approaches: | ||
|
|
||
| - **git-notes** (`refs/notes/squad`): State stored in git notes ref. Survives | ||
| branch switches, invisible in diffs and PRs. | ||
| - **orphan-branch** (`squad-state`): Dedicated orphan branch with no common | ||
| ancestor. State files never appear in main. | ||
|
|
||
| Configure via `.squad/config.json`: `{ "stateBackend": "git-notes" }` or | ||
| the `--state-backend` CLI flag. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -250,6 +250,7 @@ async function main(): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ${BOLD}--global${RESET} Use personal (global) squad path (for init, upgrade)`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ${BOLD}--economy${RESET} Activate economy mode for this session (cheaper models)`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ${BOLD}--team-root${RESET} Override team root path for resolution`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ${BOLD}--state-backend${RESET} State storage: worktree | external | git-notes | orphan`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`\nInstallation:`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` npm install --save-dev @bradygaster/squad-cli`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`\nInsider channel:`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -295,8 +296,33 @@ async function main(): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const noWorkflows = args.includes('--no-workflows'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sdk = args.includes('--sdk'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const roles = args.includes('--roles'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --state-backend: write stateBackend into .squad/config.json on init | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateBackendIdx = args.indexOf('--state-backend'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateBackendVal = (stateBackendIdx !== -1 && args[stateBackendIdx + 1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? args[stateBackendIdx + 1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global init: suppress workflows (no GitHub CI in ~/.config/squad/) and bootstrap personal squad | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runInit(dest, { includeWorkflows: !noWorkflows && !hasGlobal, sdk, roles, isGlobal: hasGlobal }).catch(err => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runInit(dest, { includeWorkflows: !noWorkflows && !hasGlobal, sdk, roles, isGlobal: hasGlobal }).then(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (stateBackendVal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { join } = await import('node:path'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const squadDir = join(dest, '.squad'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(squadDir)) mkdirSync(squadDir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const configPath = join(squadDir, 'config.json'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read existing config first, then merge (avoids overwriting unrelated keys) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let config: Record<string, unknown> = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(configPath)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config = JSON.parse(readFileSync(configPath, 'utf-8')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { /* fresh config */ } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config['stateBackend'] = stateBackendVal; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`✓ State backend set to '${stateBackendVal}' in .squad/config.json`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).catch(err => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fatal(err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+307
to
327
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runInit(dest, { includeWorkflows: !noWorkflows && !hasGlobal, sdk, roles, isGlobal: hasGlobal }).then(async () => { | |
| if (stateBackendVal) { | |
| const { join } = await import('node:path'); | |
| const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs'); | |
| const squadDir = join(dest, '.squad'); | |
| if (!existsSync(squadDir)) mkdirSync(squadDir, { recursive: true }); | |
| const configPath = join(squadDir, 'config.json'); | |
| let config: Record<string, unknown> = {}; | |
| try { | |
| if (existsSync(configPath)) { | |
| config = JSON.parse(readFileSync(configPath, 'utf-8')); | |
| } | |
| } catch { /* fresh config */ } | |
| config['stateBackend'] = stateBackendVal; | |
| writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); | |
| console.log(`✓ State backend set to '${stateBackendVal}' in .squad/config.json`); | |
| } | |
| }).catch(err => { | |
| fatal(err.message); | |
| }); | |
| try { | |
| await runInit(dest, { includeWorkflows: !noWorkflows && !hasGlobal, sdk, roles, isGlobal: hasGlobal }); | |
| if (stateBackendVal) { | |
| const { join } = await import('node:path'); | |
| const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('node:fs'); | |
| const squadDir = join(dest, '.squad'); | |
| if (!existsSync(squadDir)) mkdirSync(squadDir, { recursive: true }); | |
| const configPath = join(squadDir, 'config.json'); | |
| if (!existsSync(configPath)) { | |
| fatal('Failed to set state backend: .squad/config.json was not created by init.'); | |
| } | |
| let parsedConfig: unknown; | |
| try { | |
| parsedConfig = JSON.parse(readFileSync(configPath, 'utf-8')); | |
| } catch { | |
| fatal('Failed to set state backend: existing .squad/config.json contains malformed JSON. Please fix it manually and retry.'); | |
| } | |
| if (!parsedConfig || typeof parsedConfig !== 'object' || Array.isArray(parsedConfig)) { | |
| fatal('Failed to set state backend: .squad/config.json must contain a JSON object.'); | |
| } | |
| const config = parsedConfig as Record<string, unknown>; | |
| if (!('version' in config) || !('teamRoot' in config)) { | |
| fatal('Failed to set state backend: .squad/config.json is missing required fields (version, teamRoot).'); | |
| } | |
| config['stateBackend'] = stateBackendVal; | |
| writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); | |
| console.log(`✓ State backend set to '${stateBackendVal}' in .squad/config.json`); | |
| } | |
| } catch (err) { | |
| fatal(err instanceof Error ? err.message : String(err)); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ export interface SquadDirConfig { | |
| consult?: boolean; | ||
| /** True when extraction is disabled for consult sessions (read-only consultation) */ | ||
| extractionDisabled?: boolean; | ||
| /** State storage backend: worktree | external | git-notes | orphan */ | ||
| stateBackend?: string; | ||
| } | ||
|
Comment on lines
34
to
39
|
||
|
|
||
| /** | ||
|
|
@@ -222,6 +224,7 @@ export function loadDirConfig(squadDir: string): SquadDirConfig | null { | |
| projectKey: typeof parsed.projectKey === 'string' ? parsed.projectKey : null, | ||
| consult: parsed.consult === true ? true : undefined, | ||
| extractionDisabled: parsed.extractionDisabled === true ? true : undefined, | ||
| stateBackend: typeof parsed.stateBackend === 'string' ? parsed.stateBackend : undefined, | ||
| }; | ||
| } | ||
| return null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--state-backendparsing accepts whatever token follows, including another flag when the value is omitted (e.g.--state-backend --sdk). Validate that the next arg exists, does not start with-, and is one of the supported backend values before using it.