Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion agents/gsd-executor.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Load execution context:
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "${PHASE}")
```

Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `plans`, `incomplete_plans`.
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `plans`, `incomplete_plans`.

Also read STATE.md for position, decisions, blockers:
```bash
Expand Down Expand Up @@ -320,6 +320,14 @@ git add src/types/user.ts
| `chore` | Config, tooling, dependencies |

**4. Commit:**

**If `sub_repos` is configured (non-empty array from init context):** Use `commit-to-subrepo` to route files to their correct sub-repo:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {concise task description}" --files file1 file2 ...
```
Returns JSON with per-repo commit hashes: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`. Record all hashes for SUMMARY.

**Otherwise (standard single-repo):**
```bash
git commit -m "{type}({phase}-{plan}): {concise task description}

Expand Down
9 changes: 9 additions & 0 deletions get-shit-done/bin/gsd-tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* resolve-model <agent-type> Get model for agent based on profile
* find-phase <phase> Find phase directory by number
* commit <message> [--files f1 f2] Commit planning docs
* commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
* verify-summary <path> Verify a SUMMARY.md file
* generate-slug <text> Convert text to URL-safe slug
* current-timestamp [format] Get timestamp (full|date|filename)
Expand Down Expand Up @@ -271,6 +272,14 @@ async function main() {
break;
}

case 'commit-to-subrepo': {
const message = args[1];
const filesIndex = args.indexOf('--files');
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
commands.cmdCommitToSubrepo(cwd, message, files, raw);
break;
}

case 'verify-summary': {
const summaryPath = args[1];
const countIndex = args.indexOf('--check-count');
Expand Down
65 changes: 65 additions & 0 deletions get-shit-done/bin/lib/commands.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,70 @@ function cmdCommit(cwd, message, files, raw, amend) {
output(result, raw, hash || 'committed');
}

function cmdCommitToSubrepo(cwd, message, files, raw) {
if (!message) {
error('commit message required');
}

const config = loadConfig(cwd);
const subRepos = config.sub_repos;

if (!subRepos || subRepos.length === 0) {
error('no sub_repos configured in .planning/config.json');
}

if (!files || files.length === 0) {
error('--files required for commit-to-subrepo');
}

// Group files by sub-repo prefix
const grouped = {};
const unmatched = [];
for (const file of files) {
const match = subRepos.find(repo => file.startsWith(repo + '/'));
if (match) {
if (!grouped[match]) grouped[match] = [];
grouped[match].push(file);
} else {
unmatched.push(file);
}
}

const repos = {};
for (const [repo, repoFiles] of Object.entries(grouped)) {
const repoCwd = path.join(cwd, repo);

// Stage files (strip sub-repo prefix for paths relative to that repo)
for (const file of repoFiles) {
const relativePath = file.slice(repo.length + 1);
execGit(repoCwd, ['add', relativePath]);
}

// Commit
const commitResult = execGit(repoCwd, ['commit', '-m', message]);
if (commitResult.exitCode !== 0) {
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
continue;
}
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr };
continue;
}

// Get hash
const hashResult = execGit(repoCwd, ['rev-parse', '--short', 'HEAD']);
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
repos[repo] = { committed: true, hash, files: repoFiles };
}

const result = {
committed: Object.values(repos).some(r => r.committed),
repos,
unmatched: unmatched.length > 0 ? unmatched : undefined,
};
output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' '));
}

function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
if (!summaryPath) {
error('summary-path required for summary-extract');
Expand Down Expand Up @@ -540,6 +604,7 @@ module.exports = {
cmdHistoryDigest,
cmdResolveModel,
cmdCommit,
cmdCommitToSubrepo,
cmdSummaryExtract,
cmdWebsearch,
cmdProgressRender,
Expand Down
2 changes: 2 additions & 0 deletions get-shit-done/bin/lib/core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function loadConfig(cwd) {
nyquist_validation: false,
parallelization: true,
brave_search: false,
sub_repos: [],
};

try {
Expand Down Expand Up @@ -113,6 +114,7 @@ function loadConfig(cwd) {
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
parallelization,
brave_search: get('brave_search') ?? defaults.brave_search,
sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
model_overrides: parsed.model_overrides || null,
};
} catch {
Expand Down
43 changes: 43 additions & 0 deletions get-shit-done/references/git-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,46 @@ Each plan produces 2-4 commits (tasks + metadata). Clear, granular, bisectable.
- "Commit noise" irrelevant when consumer is Claude, not humans

</commit_strategy_rationale>

<sub_repos_support>

## Multi-Repo Workspace Support (sub_repos)

For workspaces with separate git repos (e.g., `backend/`, `frontend/`, `shared/`), GSD routes commits to each repo independently.

### Configuration

In `.planning/config.json`, list sub-repo directories under `planning.sub_repos`:

```json
{
"planning": {
"commit_docs": false,
"sub_repos": ["backend", "frontend", "shared"]
}
}
```

Set `commit_docs: false` so planning docs stay local and are not committed to any sub-repo.

### How It Works

1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos.
2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo).
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.

### Commit Routing

Instead of the standard `commit` command, use `commit-to-subrepo` when `sub_repos` is configured:

```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "feat(02-01): add user API" \
--files backend/src/api/users.ts backend/src/types/user.ts frontend/src/components/UserForm.tsx
```

This stages `src/api/users.ts` and `src/types/user.ts` in the `backend/` repo, and `src/components/UserForm.tsx` in the `frontend/` repo, then commits each independently with the same message.

Files that don't match any configured sub-repo are reported as unmatched.

</sub_repos_support>
3 changes: 2 additions & 1 deletion get-shit-done/templates/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"planning": {
"commit_docs": true,
"search_gitignored": false
"search_gitignored": false,
"sub_repos": []
},
"parallelization": {
"enabled": true,
Expand Down
16 changes: 15 additions & 1 deletion get-shit-done/workflows/execute-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Load execution context (paths only to minimize orchestrator context):
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "${PHASE}")
```

Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.

If `.planning/` missing: error.
</step>
Expand Down Expand Up @@ -253,6 +253,20 @@ git add src/types/user.ts

**4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.

<sub_repos_commit_flow>
**Sub-repos mode:** If `sub_repos` is configured (non-empty array from init context), use `commit-to-subrepo` instead of standard git commit. This routes files to their correct sub-repo based on path prefix.

```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {description}" --files file1 file2 ...
```

The command groups files by sub-repo prefix and commits atomically to each. Returns JSON: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`.

Record hashes from each repo in the response for SUMMARY tracking.

**If `sub_repos` is empty or not set:** Use standard git commit flow below.
</sub_repos_commit_flow>

**5. Record hash:**
```bash
TASK_COMMIT=$(git rev-parse --short HEAD)
Expand Down
33 changes: 33 additions & 0 deletions get-shit-done/workflows/new-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,39 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project

**Note:** Run `/gsd:settings` anytime to update these preferences.

## 5.1. Sub-Repo Detection

**Detect multi-repo workspace:**

Check for directories with their own `.git` folders (separate repos within the workspace):

```bash
find . -maxdepth 2 -type d -name ".git" -not -path "./.git"
```

**If sub-repos found:**

Strip the `/.git` suffix and `./` prefix to get directory names (e.g., `./backend/.git` → `backend`).

Use AskUserQuestion:
- header: "Multi-Repo Workspace"
- question: "I detected separate git repos in this workspace. Which directories contain code that GSD should commit to?"
- multiSelect: true
- options: one option per detected directory
- "[directory name]" — Separate git repo

**If user selects one or more directories:**
- Set `planning.sub_repos` in config.json to the selected directory names array (e.g., `["backend", "frontend"]`)
- Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces)
- Add `.planning/` to `.gitignore` if not already present

Update the config file:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "chore: configure multi-repo workspace" --files .planning/config.json
```

**If no sub-repos found or user selects none:** Continue with no changes to config.

## 5.5. Resolve Model Profile

Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model`.
Expand Down
Loading