the-grid-cc 1.7.13 → 1.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/02-SUMMARY.md +156 -0
- package/agents/grid-accountant.md +519 -0
- package/agents/grid-git-operator.md +661 -0
- package/agents/grid-researcher.md +421 -0
- package/agents/grid-scout.md +376 -0
- package/commands/grid/VERSION +1 -1
- package/commands/grid/branch.md +567 -0
- package/commands/grid/budget.md +438 -0
- package/commands/grid/daemon.md +637 -0
- package/commands/grid/init.md +375 -18
- package/commands/grid/mc.md +103 -1098
- package/commands/grid/resume.md +656 -0
- package/docs/BUDGET_SYSTEM.md +745 -0
- package/docs/DAEMON_ARCHITECTURE.md +780 -0
- package/docs/GIT_AUTONOMY.md +981 -0
- package/docs/MC_OPTIMIZATION.md +181 -0
- package/docs/MC_PROTOCOLS.md +950 -0
- package/docs/PERSISTENCE.md +962 -0
- package/docs/RESEARCH_FIRST.md +591 -0
- package/package.json +1 -1
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
# Git Autonomy Protocol - Technical Design Document
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Author:** Grid Git Operator Program
|
|
5
|
+
**Date:** 2026-01-23
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
The Git Autonomy Protocol enables The Grid to manage git operations fully autonomously while maintaining safety guarantees. This document specifies the technical design for automatic branch creation, atomic commits, PR creation, conflict resolution, and safe push operations.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
1. [Design Principles](#design-principles)
|
|
18
|
+
2. [Architecture Overview](#architecture-overview)
|
|
19
|
+
3. [Branch Management](#branch-management)
|
|
20
|
+
4. [Commit Protocol](#commit-protocol)
|
|
21
|
+
5. [Push Automation](#push-automation)
|
|
22
|
+
6. [Conflict Resolution](#conflict-resolution)
|
|
23
|
+
7. [PR Automation](#pr-automation)
|
|
24
|
+
8. [Safety Guarantees](#safety-guarantees)
|
|
25
|
+
9. [Integration Points](#integration-points)
|
|
26
|
+
10. [Configuration](#configuration)
|
|
27
|
+
11. [Error Handling](#error-handling)
|
|
28
|
+
12. [Future Enhancements](#future-enhancements)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Design Principles
|
|
33
|
+
|
|
34
|
+
### 1. Safety First
|
|
35
|
+
No autonomous operation should risk data loss or corrupt git history. Protected branch rules are enforced at the protocol level, not by convention.
|
|
36
|
+
|
|
37
|
+
### 2. Atomic Operations
|
|
38
|
+
Each git operation is atomic and recoverable. Partial commits or incomplete pushes leave the repository in a known good state.
|
|
39
|
+
|
|
40
|
+
### 3. Transparency
|
|
41
|
+
All git operations are logged and visible. Users can always see what The Grid did to their repository.
|
|
42
|
+
|
|
43
|
+
### 4. Graceful Degradation
|
|
44
|
+
When autonomous operation fails (conflicts, auth issues), the system gracefully falls back to manual intervention with clear guidance.
|
|
45
|
+
|
|
46
|
+
### 5. Convention Over Configuration
|
|
47
|
+
Sensible defaults enable zero-configuration usage while allowing customization for power users.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Architecture Overview
|
|
52
|
+
|
|
53
|
+
### Component Diagram
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
+-------------------+
|
|
57
|
+
| Master Control |
|
|
58
|
+
| (Orchestrator) |
|
|
59
|
+
+--------+----------+
|
|
60
|
+
|
|
|
61
|
+
| spawns
|
|
62
|
+
v
|
|
63
|
+
+-------------------+ +------------------+
|
|
64
|
+
| Git Operator |<--->| Local Git Repo |
|
|
65
|
+
| (Agent) | +------------------+
|
|
66
|
+
+--------+----------+ |
|
|
67
|
+
| | remote
|
|
68
|
+
| uses v
|
|
69
|
+
v +------------------+
|
|
70
|
+
+-------------------+ | GitHub/GitLab |
|
|
71
|
+
| /grid:branch | | (Remote) |
|
|
72
|
+
| (Command) | +------------------+
|
|
73
|
+
+-------------------+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Responsibility Matrix
|
|
77
|
+
|
|
78
|
+
| Component | Responsibilities |
|
|
79
|
+
|-----------|------------------|
|
|
80
|
+
| Master Control | Session lifecycle, spawning operators |
|
|
81
|
+
| Git Operator | All git operations, safety enforcement |
|
|
82
|
+
| /grid:branch | User-facing branch management commands |
|
|
83
|
+
| Executor | Requests commits via Git Operator |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Branch Management
|
|
88
|
+
|
|
89
|
+
### Automatic Branch Creation
|
|
90
|
+
|
|
91
|
+
When a Grid session starts on a protected branch, the system automatically creates a feature branch:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
def ensure_feature_branch(cluster_name: str) -> str:
|
|
95
|
+
"""Ensure we're on a feature branch, create if needed."""
|
|
96
|
+
|
|
97
|
+
current = get_current_branch()
|
|
98
|
+
|
|
99
|
+
# Already on feature branch
|
|
100
|
+
if not is_protected(current):
|
|
101
|
+
return current
|
|
102
|
+
|
|
103
|
+
# Generate branch name from cluster
|
|
104
|
+
slug = slugify(cluster_name) # "React Todo App" -> "react-todo-app"
|
|
105
|
+
branch_name = f"grid/{slug}"
|
|
106
|
+
|
|
107
|
+
# Handle existing branch with same name
|
|
108
|
+
if branch_exists(branch_name):
|
|
109
|
+
branch_name = f"{branch_name}-{timestamp()}"
|
|
110
|
+
|
|
111
|
+
# Create and switch
|
|
112
|
+
git_checkout_b(branch_name)
|
|
113
|
+
|
|
114
|
+
return branch_name
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Branch Naming Algorithm
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
def generate_branch_name(context: GridContext) -> str:
|
|
121
|
+
"""Generate appropriate branch name for context."""
|
|
122
|
+
|
|
123
|
+
prefix = config.get("branch_prefix", "grid/")
|
|
124
|
+
|
|
125
|
+
if context.type == "cluster":
|
|
126
|
+
# Full project work
|
|
127
|
+
base = slugify(context.cluster_name)
|
|
128
|
+
elif context.type == "quick":
|
|
129
|
+
# Quick task
|
|
130
|
+
base = slugify(context.task_description[:30])
|
|
131
|
+
elif context.type == "debug":
|
|
132
|
+
# Debug session
|
|
133
|
+
base = f"fix-{slugify(context.symptoms[0][:20])}"
|
|
134
|
+
else:
|
|
135
|
+
# Fallback
|
|
136
|
+
base = f"work-{date_slug()}"
|
|
137
|
+
|
|
138
|
+
# Ensure uniqueness
|
|
139
|
+
candidate = f"{prefix}{base}"
|
|
140
|
+
if branch_exists(candidate):
|
|
141
|
+
candidate = f"{candidate}-{short_hash()}"
|
|
142
|
+
|
|
143
|
+
return candidate
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Protected Branch Detection
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
PROTECTED_BRANCHES = ["main", "master", "production", "release/*"]
|
|
150
|
+
|
|
151
|
+
def is_protected(branch: str) -> bool:
|
|
152
|
+
"""Check if branch is protected."""
|
|
153
|
+
|
|
154
|
+
# Check config overrides
|
|
155
|
+
protected = config.get("protected_branches", PROTECTED_BRANCHES)
|
|
156
|
+
|
|
157
|
+
for pattern in protected:
|
|
158
|
+
if pattern.endswith("/*"):
|
|
159
|
+
# Wildcard match
|
|
160
|
+
prefix = pattern[:-2]
|
|
161
|
+
if branch.startswith(prefix):
|
|
162
|
+
return True
|
|
163
|
+
elif branch == pattern:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
return False
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Commit Protocol
|
|
172
|
+
|
|
173
|
+
### Atomic Commits per Thread
|
|
174
|
+
|
|
175
|
+
Each thread in a Grid plan produces exactly one commit:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
def commit_thread(thread: Thread, files: List[str], message: CommitMessage) -> str:
|
|
179
|
+
"""Create atomic commit for a single thread."""
|
|
180
|
+
|
|
181
|
+
# Validate files exist
|
|
182
|
+
for file in files:
|
|
183
|
+
if not os.path.exists(file):
|
|
184
|
+
raise CommitError(f"File not found: {file}")
|
|
185
|
+
|
|
186
|
+
# Stage files individually (NEVER git add .)
|
|
187
|
+
for file in files:
|
|
188
|
+
git_add(file)
|
|
189
|
+
|
|
190
|
+
# Verify staged matches expected
|
|
191
|
+
staged = get_staged_files()
|
|
192
|
+
if set(staged) != set(files):
|
|
193
|
+
raise CommitError(f"Staged files mismatch. Expected: {files}, Got: {staged}")
|
|
194
|
+
|
|
195
|
+
# Create commit
|
|
196
|
+
commit_hash = git_commit(format_message(message))
|
|
197
|
+
|
|
198
|
+
return commit_hash
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Commit Message Format
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
@dataclass
|
|
205
|
+
class CommitMessage:
|
|
206
|
+
type: str # feat, fix, refactor, etc.
|
|
207
|
+
scope: str # block-01, quick, debug
|
|
208
|
+
subject: str # Brief description (50 chars)
|
|
209
|
+
body: List[str] # Bullet points of changes
|
|
210
|
+
|
|
211
|
+
def format_message(msg: CommitMessage) -> str:
|
|
212
|
+
"""Format commit message according to convention."""
|
|
213
|
+
|
|
214
|
+
# Header line
|
|
215
|
+
header = f"{msg.type}({msg.scope}): {msg.subject}"
|
|
216
|
+
|
|
217
|
+
if len(header) > 72:
|
|
218
|
+
raise CommitError(f"Header too long: {len(header)} chars (max 72)")
|
|
219
|
+
|
|
220
|
+
# Body
|
|
221
|
+
body_lines = [f"- {line}" for line in msg.body]
|
|
222
|
+
body = "\n".join(body_lines)
|
|
223
|
+
|
|
224
|
+
return f"{header}\n\n{body}"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Commit Types Taxonomy
|
|
228
|
+
|
|
229
|
+
| Type | Description | Example |
|
|
230
|
+
|------|-------------|---------|
|
|
231
|
+
| `feat` | New feature or capability | `feat(auth): add JWT token refresh` |
|
|
232
|
+
| `fix` | Bug fix | `fix(api): handle null user response` |
|
|
233
|
+
| `refactor` | Code restructure, no behavior change | `refactor(utils): simplify date formatting` |
|
|
234
|
+
| `perf` | Performance improvement | `perf(db): add index on user_id` |
|
|
235
|
+
| `test` | Test additions/modifications | `test(auth): add login flow tests` |
|
|
236
|
+
| `docs` | Documentation only | `docs(api): update endpoint descriptions` |
|
|
237
|
+
| `chore` | Tooling, deps, config | `chore(deps): upgrade react to 19` |
|
|
238
|
+
| `style` | Formatting, no code change | `style(lint): apply prettier` |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Push Automation
|
|
243
|
+
|
|
244
|
+
### Push Timing Strategies
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
class PushStrategy(Enum):
|
|
248
|
+
IMMEDIATE = "immediate" # After every commit
|
|
249
|
+
WAVE = "wave" # After each wave completes
|
|
250
|
+
BLOCK = "block" # After each block completes
|
|
251
|
+
MANUAL = "manual" # Never auto-push
|
|
252
|
+
|
|
253
|
+
def should_push(strategy: PushStrategy, event: GridEvent) -> bool:
|
|
254
|
+
"""Determine if we should push based on strategy and event."""
|
|
255
|
+
|
|
256
|
+
match strategy:
|
|
257
|
+
case PushStrategy.IMMEDIATE:
|
|
258
|
+
return event.type == "commit"
|
|
259
|
+
case PushStrategy.WAVE:
|
|
260
|
+
return event.type == "wave_complete"
|
|
261
|
+
case PushStrategy.BLOCK:
|
|
262
|
+
return event.type == "block_complete"
|
|
263
|
+
case PushStrategy.MANUAL:
|
|
264
|
+
return False
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Safe Push Protocol
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def safe_push(branch: str, remote: str = "origin") -> PushResult:
|
|
271
|
+
"""Push with safety checks."""
|
|
272
|
+
|
|
273
|
+
# Pre-flight checks
|
|
274
|
+
if is_protected(branch):
|
|
275
|
+
raise PushError("Cannot push to protected branch")
|
|
276
|
+
|
|
277
|
+
# Fetch current remote state
|
|
278
|
+
git_fetch(remote, branch)
|
|
279
|
+
|
|
280
|
+
# Analyze divergence
|
|
281
|
+
divergence = analyze_divergence(branch, f"{remote}/{branch}")
|
|
282
|
+
|
|
283
|
+
match divergence:
|
|
284
|
+
case Divergence.UP_TO_DATE:
|
|
285
|
+
return PushResult(status="nothing_to_push")
|
|
286
|
+
|
|
287
|
+
case Divergence.AHEAD:
|
|
288
|
+
# Safe to push
|
|
289
|
+
git_push(remote, branch)
|
|
290
|
+
return PushResult(status="pushed")
|
|
291
|
+
|
|
292
|
+
case Divergence.BEHIND:
|
|
293
|
+
# Need to pull first
|
|
294
|
+
git_pull_rebase(remote, branch)
|
|
295
|
+
git_push(remote, branch)
|
|
296
|
+
return PushResult(status="pulled_and_pushed")
|
|
297
|
+
|
|
298
|
+
case Divergence.DIVERGED:
|
|
299
|
+
# Attempt auto-merge
|
|
300
|
+
result = attempt_auto_merge(remote, branch)
|
|
301
|
+
if result.success:
|
|
302
|
+
git_push(remote, branch)
|
|
303
|
+
return PushResult(status="merged_and_pushed")
|
|
304
|
+
else:
|
|
305
|
+
return PushResult(status="conflict", conflicts=result.conflicts)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Divergence Analysis
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
class Divergence(Enum):
|
|
312
|
+
UP_TO_DATE = "up_to_date"
|
|
313
|
+
AHEAD = "ahead"
|
|
314
|
+
BEHIND = "behind"
|
|
315
|
+
DIVERGED = "diverged"
|
|
316
|
+
|
|
317
|
+
def analyze_divergence(local: str, remote: str) -> Divergence:
|
|
318
|
+
"""Analyze how local and remote have diverged."""
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
base = git_merge_base(local, remote)
|
|
322
|
+
except GitError:
|
|
323
|
+
# Remote doesn't exist yet
|
|
324
|
+
return Divergence.AHEAD
|
|
325
|
+
|
|
326
|
+
local_head = git_rev_parse(local)
|
|
327
|
+
remote_head = git_rev_parse(remote)
|
|
328
|
+
|
|
329
|
+
if local_head == remote_head:
|
|
330
|
+
return Divergence.UP_TO_DATE
|
|
331
|
+
elif local_head == base:
|
|
332
|
+
return Divergence.BEHIND
|
|
333
|
+
elif remote_head == base:
|
|
334
|
+
return Divergence.AHEAD
|
|
335
|
+
else:
|
|
336
|
+
return Divergence.DIVERGED
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Conflict Resolution
|
|
342
|
+
|
|
343
|
+
### Conflict Detection
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
def detect_conflicts(ours: str, theirs: str) -> List[ConflictFile]:
|
|
347
|
+
"""Detect which files would conflict in a merge."""
|
|
348
|
+
|
|
349
|
+
base = git_merge_base(ours, theirs)
|
|
350
|
+
|
|
351
|
+
# Use git merge-tree for dry-run conflict detection
|
|
352
|
+
result = git_merge_tree(base, ours, theirs)
|
|
353
|
+
|
|
354
|
+
conflicts = []
|
|
355
|
+
for file in parse_merge_tree_output(result):
|
|
356
|
+
if file.has_conflict:
|
|
357
|
+
conflicts.append(ConflictFile(
|
|
358
|
+
path=file.path,
|
|
359
|
+
ours_change=file.ours_change,
|
|
360
|
+
theirs_change=file.theirs_change,
|
|
361
|
+
conflict_type=classify_conflict(file)
|
|
362
|
+
))
|
|
363
|
+
|
|
364
|
+
return conflicts
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Conflict Classification
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
class ConflictType(Enum):
|
|
371
|
+
BOTH_MODIFIED = "both_modified" # Same file changed differently
|
|
372
|
+
DELETE_MODIFY = "delete_modify" # One deleted, one modified
|
|
373
|
+
ADD_ADD = "add_add" # Both added same file
|
|
374
|
+
RENAME_RENAME = "rename_rename" # Both renamed differently
|
|
375
|
+
|
|
376
|
+
def classify_conflict(file: ConflictFile) -> ConflictType:
|
|
377
|
+
"""Classify the type of conflict for resolution strategy."""
|
|
378
|
+
|
|
379
|
+
if file.ours_deleted and file.theirs_modified:
|
|
380
|
+
return ConflictType.DELETE_MODIFY
|
|
381
|
+
elif file.theirs_deleted and file.ours_modified:
|
|
382
|
+
return ConflictType.DELETE_MODIFY
|
|
383
|
+
elif file.ours_added and file.theirs_added:
|
|
384
|
+
return ConflictType.ADD_ADD
|
|
385
|
+
elif file.ours_renamed and file.theirs_renamed:
|
|
386
|
+
return ConflictType.RENAME_RENAME
|
|
387
|
+
else:
|
|
388
|
+
return ConflictType.BOTH_MODIFIED
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Auto-Resolution Strategies
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
def attempt_auto_resolve(conflict: ConflictFile) -> Optional[Resolution]:
|
|
395
|
+
"""Attempt to auto-resolve a conflict if safe."""
|
|
396
|
+
|
|
397
|
+
# Only auto-resolve non-destructive cases
|
|
398
|
+
match conflict.type:
|
|
399
|
+
case ConflictType.BOTH_MODIFIED:
|
|
400
|
+
# Check if changes are in different regions
|
|
401
|
+
if non_overlapping_changes(conflict):
|
|
402
|
+
return merge_non_overlapping(conflict)
|
|
403
|
+
else:
|
|
404
|
+
return None # Manual resolution needed
|
|
405
|
+
|
|
406
|
+
case ConflictType.ADD_ADD:
|
|
407
|
+
# Both added same file - check if identical
|
|
408
|
+
if files_identical(conflict.ours, conflict.theirs):
|
|
409
|
+
return Resolution(keep="ours", reason="identical")
|
|
410
|
+
else:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
case _:
|
|
414
|
+
return None # All other cases need manual resolution
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Manual Resolution Workflow
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
def request_manual_resolution(conflicts: List[ConflictFile]) -> ConflictCheckpoint:
|
|
421
|
+
"""Create checkpoint for manual conflict resolution."""
|
|
422
|
+
|
|
423
|
+
return ConflictCheckpoint(
|
|
424
|
+
type="conflict",
|
|
425
|
+
conflicts=[
|
|
426
|
+
{
|
|
427
|
+
"file": c.path,
|
|
428
|
+
"ours": c.ours_change,
|
|
429
|
+
"theirs": c.theirs_change,
|
|
430
|
+
"type": c.conflict_type.value
|
|
431
|
+
}
|
|
432
|
+
for c in conflicts
|
|
433
|
+
],
|
|
434
|
+
options=[
|
|
435
|
+
Option("keep_ours", "Keep our changes, discard theirs"),
|
|
436
|
+
Option("keep_theirs", "Keep their changes, discard ours"),
|
|
437
|
+
Option("manual", "Open files and resolve manually"),
|
|
438
|
+
Option("abort", "Abort merge, keep current state")
|
|
439
|
+
],
|
|
440
|
+
instructions="""
|
|
441
|
+
To resolve manually:
|
|
442
|
+
1. Open conflicting files
|
|
443
|
+
2. Look for <<<<<<< and >>>>>>> markers
|
|
444
|
+
3. Edit to desired state
|
|
445
|
+
4. Remove conflict markers
|
|
446
|
+
5. Stage resolved files: git add {file}
|
|
447
|
+
6. Continue: git merge --continue
|
|
448
|
+
"""
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## PR Automation
|
|
455
|
+
|
|
456
|
+
### PR Content Generation
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
def generate_pr_content(branch: str, base: str = "main") -> PRContent:
|
|
460
|
+
"""Generate PR title, body, and metadata."""
|
|
461
|
+
|
|
462
|
+
# Gather commit information
|
|
463
|
+
commits = git_log(f"{base}..{branch}", format="%s")
|
|
464
|
+
commit_details = git_log(f"{base}..{branch}", format="%H %s")
|
|
465
|
+
files_changed = git_diff_stat(base, branch)
|
|
466
|
+
|
|
467
|
+
# Determine PR type from commits
|
|
468
|
+
pr_type = infer_pr_type(commits)
|
|
469
|
+
|
|
470
|
+
# Generate title
|
|
471
|
+
if len(commits) == 1:
|
|
472
|
+
title = commits[0]
|
|
473
|
+
else:
|
|
474
|
+
title = f"{pr_type}: {summarize_commits(commits)}"
|
|
475
|
+
|
|
476
|
+
# Generate body
|
|
477
|
+
body = generate_pr_body(
|
|
478
|
+
commits=commit_details,
|
|
479
|
+
files_changed=files_changed,
|
|
480
|
+
grid_metadata=read_grid_state()
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return PRContent(title=title, body=body, labels=infer_labels(commits))
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### PR Body Template
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
PR_BODY_TEMPLATE = """
|
|
490
|
+
## Summary
|
|
491
|
+
{summary}
|
|
492
|
+
|
|
493
|
+
## Changes
|
|
494
|
+
{commit_list}
|
|
495
|
+
|
|
496
|
+
## Files Changed
|
|
497
|
+
{files_stat}
|
|
498
|
+
|
|
499
|
+
## Grid Session
|
|
500
|
+
- **Cluster:** {cluster_name}
|
|
501
|
+
- **Blocks completed:** {blocks_completed}
|
|
502
|
+
- **Total commits:** {commit_count}
|
|
503
|
+
|
|
504
|
+
## Test Plan
|
|
505
|
+
{test_plan}
|
|
506
|
+
|
|
507
|
+
## Screenshots
|
|
508
|
+
{screenshots}
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
*Automatically generated by The Grid*
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
def generate_pr_body(commits, files_changed, grid_metadata) -> str:
|
|
515
|
+
"""Fill in PR body template."""
|
|
516
|
+
|
|
517
|
+
summary = generate_summary(commits)
|
|
518
|
+
commit_list = format_commit_list(commits)
|
|
519
|
+
|
|
520
|
+
# Extract test plan from Grid summaries
|
|
521
|
+
test_plan = extract_test_plan(grid_metadata)
|
|
522
|
+
|
|
523
|
+
# Check for screenshots
|
|
524
|
+
screenshots = find_screenshots()
|
|
525
|
+
screenshot_section = format_screenshots(screenshots) if screenshots else "_No UI changes_"
|
|
526
|
+
|
|
527
|
+
return PR_BODY_TEMPLATE.format(
|
|
528
|
+
summary=summary,
|
|
529
|
+
commit_list=commit_list,
|
|
530
|
+
files_stat=files_changed,
|
|
531
|
+
cluster_name=grid_metadata.get("cluster", "N/A"),
|
|
532
|
+
blocks_completed=grid_metadata.get("blocks_completed", "N/A"),
|
|
533
|
+
commit_count=len(commits),
|
|
534
|
+
test_plan=test_plan,
|
|
535
|
+
screenshots=screenshot_section
|
|
536
|
+
)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### GitHub CLI Integration
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
def create_pr(content: PRContent, draft: bool = False) -> PRResult:
|
|
543
|
+
"""Create PR using GitHub CLI."""
|
|
544
|
+
|
|
545
|
+
# Ensure branch is pushed
|
|
546
|
+
ensure_pushed()
|
|
547
|
+
|
|
548
|
+
# Check for existing PR
|
|
549
|
+
existing = gh_pr_view()
|
|
550
|
+
if existing:
|
|
551
|
+
return PRResult(
|
|
552
|
+
status="exists",
|
|
553
|
+
number=existing.number,
|
|
554
|
+
url=existing.url
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Create PR
|
|
558
|
+
args = [
|
|
559
|
+
"gh", "pr", "create",
|
|
560
|
+
"--title", content.title,
|
|
561
|
+
"--body", content.body,
|
|
562
|
+
"--base", content.base
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
if draft:
|
|
566
|
+
args.append("--draft")
|
|
567
|
+
|
|
568
|
+
if content.labels:
|
|
569
|
+
args.extend(["--label", ",".join(content.labels)])
|
|
570
|
+
|
|
571
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
572
|
+
|
|
573
|
+
if result.returncode != 0:
|
|
574
|
+
raise PRError(result.stderr)
|
|
575
|
+
|
|
576
|
+
# Parse PR URL from output
|
|
577
|
+
pr_url = result.stdout.strip()
|
|
578
|
+
pr_number = int(pr_url.split("/")[-1])
|
|
579
|
+
|
|
580
|
+
return PRResult(status="created", number=pr_number, url=pr_url)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Safety Guarantees
|
|
586
|
+
|
|
587
|
+
### Forbidden Operations Matrix
|
|
588
|
+
|
|
589
|
+
| Operation | Condition | Allowed |
|
|
590
|
+
|-----------|-----------|---------|
|
|
591
|
+
| `git push --force` | Never automatic | NO |
|
|
592
|
+
| `git push --force` | Explicit user confirmation | YES |
|
|
593
|
+
| `git reset --hard` | On unshared local branch | YES |
|
|
594
|
+
| `git reset --hard` | On shared/pushed branch | NO |
|
|
595
|
+
| `git branch -D` | Merged branches | YES |
|
|
596
|
+
| `git branch -D` | Unmerged branches | CONFIRM |
|
|
597
|
+
| Commit to main | Never | NO |
|
|
598
|
+
| Commit to feature | Always | YES |
|
|
599
|
+
|
|
600
|
+
### Safety Enforcement
|
|
601
|
+
|
|
602
|
+
```python
|
|
603
|
+
class SafetyGuard:
|
|
604
|
+
"""Enforces safety rules for git operations."""
|
|
605
|
+
|
|
606
|
+
FORBIDDEN_FLAGS = ["--force", "-f", "--force-with-lease", "--no-verify"]
|
|
607
|
+
|
|
608
|
+
def validate_command(self, command: List[str]) -> ValidationResult:
|
|
609
|
+
"""Validate a git command before execution."""
|
|
610
|
+
|
|
611
|
+
# Check for forbidden flags
|
|
612
|
+
for flag in self.FORBIDDEN_FLAGS:
|
|
613
|
+
if flag in command:
|
|
614
|
+
if not self.has_explicit_user_confirmation():
|
|
615
|
+
return ValidationResult(
|
|
616
|
+
allowed=False,
|
|
617
|
+
reason=f"Forbidden flag: {flag}",
|
|
618
|
+
requires_confirmation=True
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Check for protected branch operations
|
|
622
|
+
if self.is_protected_branch_operation(command):
|
|
623
|
+
return ValidationResult(
|
|
624
|
+
allowed=False,
|
|
625
|
+
reason="Operation on protected branch"
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
return ValidationResult(allowed=True)
|
|
629
|
+
|
|
630
|
+
def is_protected_branch_operation(self, command: List[str]) -> bool:
|
|
631
|
+
"""Check if command operates on protected branch."""
|
|
632
|
+
|
|
633
|
+
if command[:2] == ["git", "checkout"] and len(command) > 2:
|
|
634
|
+
target = command[2]
|
|
635
|
+
if is_protected(target):
|
|
636
|
+
return False # Checkout is OK
|
|
637
|
+
|
|
638
|
+
if command[:2] == ["git", "commit"]:
|
|
639
|
+
current = get_current_branch()
|
|
640
|
+
if is_protected(current):
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
return False
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Audit Logging
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
def audit_git_operation(operation: str, args: List[str], result: Any):
|
|
650
|
+
"""Log git operation for audit trail."""
|
|
651
|
+
|
|
652
|
+
entry = {
|
|
653
|
+
"timestamp": datetime.now().isoformat(),
|
|
654
|
+
"operation": operation,
|
|
655
|
+
"args": args,
|
|
656
|
+
"result": str(result),
|
|
657
|
+
"branch": get_current_branch(),
|
|
658
|
+
"commit": get_current_commit(),
|
|
659
|
+
"user": os.environ.get("USER", "unknown")
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# Append to audit log
|
|
663
|
+
audit_path = ".grid/git_audit.jsonl"
|
|
664
|
+
with open(audit_path, "a") as f:
|
|
665
|
+
f.write(json.dumps(entry) + "\n")
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Integration Points
|
|
671
|
+
|
|
672
|
+
### Master Control Integration
|
|
673
|
+
|
|
674
|
+
```python
|
|
675
|
+
# MC spawns Git Operator at session start
|
|
676
|
+
def start_grid_session(request: str):
|
|
677
|
+
# ... planning ...
|
|
678
|
+
|
|
679
|
+
# Ensure feature branch
|
|
680
|
+
Task(
|
|
681
|
+
prompt=f"""
|
|
682
|
+
First, read ~/.claude/agents/grid-git-operator.md for your role.
|
|
683
|
+
|
|
684
|
+
SESSION START
|
|
685
|
+
|
|
686
|
+
Cluster: {cluster_name}
|
|
687
|
+
|
|
688
|
+
Ensure we're on an appropriate feature branch.
|
|
689
|
+
Report branch status.
|
|
690
|
+
""",
|
|
691
|
+
description="Git: Branch setup"
|
|
692
|
+
)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Executor Integration
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
# Executor requests commit after thread completion
|
|
699
|
+
def complete_thread(thread: Thread, files: List[str]):
|
|
700
|
+
# ... implementation work ...
|
|
701
|
+
|
|
702
|
+
# Commit via Git Operator
|
|
703
|
+
Task(
|
|
704
|
+
prompt=f"""
|
|
705
|
+
First, read ~/.claude/agents/grid-git-operator.md for your role.
|
|
706
|
+
|
|
707
|
+
COMMIT THREAD
|
|
708
|
+
|
|
709
|
+
Thread: {thread.name}
|
|
710
|
+
Files: {files}
|
|
711
|
+
Type: {thread.commit_type}
|
|
712
|
+
Scope: {thread.block_id}
|
|
713
|
+
Description: {thread.description}
|
|
714
|
+
|
|
715
|
+
Create atomic commit.
|
|
716
|
+
""",
|
|
717
|
+
description=f"Git: Commit {thread.name}"
|
|
718
|
+
)
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Wave Completion Hook
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
# After wave completes, trigger push
|
|
725
|
+
def on_wave_complete(wave: Wave):
|
|
726
|
+
if config.get("auto_push") == "wave":
|
|
727
|
+
Task(
|
|
728
|
+
prompt=f"""
|
|
729
|
+
First, read ~/.claude/agents/grid-git-operator.md for your role.
|
|
730
|
+
|
|
731
|
+
WAVE COMPLETE
|
|
732
|
+
|
|
733
|
+
Wave: {wave.number}
|
|
734
|
+
Commits: {wave.commits}
|
|
735
|
+
|
|
736
|
+
Push to remote if configured.
|
|
737
|
+
""",
|
|
738
|
+
description=f"Git: Push wave {wave.number}"
|
|
739
|
+
)
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## Configuration
|
|
745
|
+
|
|
746
|
+
### .grid/config.json Schema
|
|
747
|
+
|
|
748
|
+
```json
|
|
749
|
+
{
|
|
750
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
751
|
+
"type": "object",
|
|
752
|
+
"properties": {
|
|
753
|
+
"git": {
|
|
754
|
+
"type": "object",
|
|
755
|
+
"properties": {
|
|
756
|
+
"auto_branch": {
|
|
757
|
+
"type": "boolean",
|
|
758
|
+
"default": true,
|
|
759
|
+
"description": "Automatically create feature branch from protected branches"
|
|
760
|
+
},
|
|
761
|
+
"branch_prefix": {
|
|
762
|
+
"type": "string",
|
|
763
|
+
"default": "grid/",
|
|
764
|
+
"description": "Prefix for auto-created branches"
|
|
765
|
+
},
|
|
766
|
+
"auto_push": {
|
|
767
|
+
"type": "string",
|
|
768
|
+
"enum": ["immediate", "wave", "block", "manual"],
|
|
769
|
+
"default": "wave",
|
|
770
|
+
"description": "When to automatically push"
|
|
771
|
+
},
|
|
772
|
+
"auto_pr": {
|
|
773
|
+
"type": "boolean",
|
|
774
|
+
"default": false,
|
|
775
|
+
"description": "Automatically create PR on session complete"
|
|
776
|
+
},
|
|
777
|
+
"protected_branches": {
|
|
778
|
+
"type": "array",
|
|
779
|
+
"items": { "type": "string" },
|
|
780
|
+
"default": ["main", "master", "production"],
|
|
781
|
+
"description": "Branches that require PRs"
|
|
782
|
+
},
|
|
783
|
+
"default_base": {
|
|
784
|
+
"type": "string",
|
|
785
|
+
"default": "main",
|
|
786
|
+
"description": "Default base branch for PRs"
|
|
787
|
+
},
|
|
788
|
+
"commit_signing": {
|
|
789
|
+
"type": "boolean",
|
|
790
|
+
"default": false,
|
|
791
|
+
"description": "Sign commits with GPG"
|
|
792
|
+
},
|
|
793
|
+
"wip_commits": {
|
|
794
|
+
"type": "boolean",
|
|
795
|
+
"default": true,
|
|
796
|
+
"description": "Create WIP commits on session pause"
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Environment Variables
|
|
805
|
+
|
|
806
|
+
| Variable | Description | Default |
|
|
807
|
+
|----------|-------------|---------|
|
|
808
|
+
| `GRID_GIT_AUTO_PUSH` | Push strategy | `wave` |
|
|
809
|
+
| `GRID_GIT_AUTO_PR` | Auto-create PRs | `false` |
|
|
810
|
+
| `GRID_GIT_BRANCH_PREFIX` | Branch name prefix | `grid/` |
|
|
811
|
+
| `GRID_GIT_PROTECTED` | Comma-separated protected branches | `main,master` |
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Error Handling
|
|
816
|
+
|
|
817
|
+
### Error Categories
|
|
818
|
+
|
|
819
|
+
```python
|
|
820
|
+
class GitError(Exception):
|
|
821
|
+
"""Base class for git errors."""
|
|
822
|
+
pass
|
|
823
|
+
|
|
824
|
+
class ProtectedBranchError(GitError):
|
|
825
|
+
"""Attempted operation on protected branch."""
|
|
826
|
+
pass
|
|
827
|
+
|
|
828
|
+
class ConflictError(GitError):
|
|
829
|
+
"""Merge conflict detected."""
|
|
830
|
+
def __init__(self, conflicts: List[ConflictFile]):
|
|
831
|
+
self.conflicts = conflicts
|
|
832
|
+
|
|
833
|
+
class AuthenticationError(GitError):
|
|
834
|
+
"""Git authentication failed."""
|
|
835
|
+
pass
|
|
836
|
+
|
|
837
|
+
class RemoteError(GitError):
|
|
838
|
+
"""Remote operation failed."""
|
|
839
|
+
pass
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Error Recovery
|
|
843
|
+
|
|
844
|
+
```python
|
|
845
|
+
def handle_git_error(error: GitError) -> ErrorRecovery:
|
|
846
|
+
"""Determine recovery strategy for git error."""
|
|
847
|
+
|
|
848
|
+
match error:
|
|
849
|
+
case ProtectedBranchError():
|
|
850
|
+
return ErrorRecovery(
|
|
851
|
+
action="create_branch",
|
|
852
|
+
message="Cannot commit to protected branch. Creating feature branch."
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
case ConflictError() as e:
|
|
856
|
+
return ErrorRecovery(
|
|
857
|
+
action="checkpoint",
|
|
858
|
+
message="Merge conflicts detected.",
|
|
859
|
+
checkpoint=request_manual_resolution(e.conflicts)
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
case AuthenticationError():
|
|
863
|
+
return ErrorRecovery(
|
|
864
|
+
action="checkpoint",
|
|
865
|
+
message="Git authentication required.",
|
|
866
|
+
checkpoint=request_auth_action()
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
case RemoteError():
|
|
870
|
+
return ErrorRecovery(
|
|
871
|
+
action="retry",
|
|
872
|
+
message="Remote operation failed. Will retry.",
|
|
873
|
+
retry_after=30
|
|
874
|
+
)
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## Future Enhancements
|
|
880
|
+
|
|
881
|
+
### Planned Features
|
|
882
|
+
|
|
883
|
+
1. **Worktree Support**
|
|
884
|
+
- Parallel development on multiple branches
|
|
885
|
+
- Each Grid session in isolated worktree
|
|
886
|
+
|
|
887
|
+
2. **Stacked PRs**
|
|
888
|
+
- Create chains of dependent PRs
|
|
889
|
+
- Auto-update stack on changes
|
|
890
|
+
|
|
891
|
+
3. **Bisect Integration**
|
|
892
|
+
- Automated git bisect for bug hunting
|
|
893
|
+
- Integration with Grid Debugger
|
|
894
|
+
|
|
895
|
+
4. **Hooks System**
|
|
896
|
+
- Pre-commit hooks for Grid validation
|
|
897
|
+
- Post-commit hooks for notifications
|
|
898
|
+
|
|
899
|
+
5. **Team Collaboration**
|
|
900
|
+
- Branch locking during Grid sessions
|
|
901
|
+
- Real-time collaboration awareness
|
|
902
|
+
|
|
903
|
+
### Research Areas
|
|
904
|
+
|
|
905
|
+
1. **Semantic Merge**
|
|
906
|
+
- Use AST analysis for smarter conflict resolution
|
|
907
|
+
- Language-aware merging
|
|
908
|
+
|
|
909
|
+
2. **Predictive Conflicts**
|
|
910
|
+
- Warn about potential conflicts before they happen
|
|
911
|
+
- Suggest preemptive rebases
|
|
912
|
+
|
|
913
|
+
3. **AI-Assisted Resolution**
|
|
914
|
+
- LLM-powered conflict resolution suggestions
|
|
915
|
+
- Context-aware merge decisions
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## Appendix A: Command Reference
|
|
920
|
+
|
|
921
|
+
### Git Operator Commands
|
|
922
|
+
|
|
923
|
+
| Command | Description |
|
|
924
|
+
|---------|-------------|
|
|
925
|
+
| `BRANCH CHECK` | Analyze current git state |
|
|
926
|
+
| `CREATE BRANCH {name}` | Create new feature branch |
|
|
927
|
+
| `COMMIT THREAD` | Create atomic commit for thread |
|
|
928
|
+
| `PUSH` | Push current branch to remote |
|
|
929
|
+
| `CREATE PR` | Create pull request |
|
|
930
|
+
| `RESOLVE CONFLICT` | Handle merge conflicts |
|
|
931
|
+
|
|
932
|
+
### /grid:branch Commands
|
|
933
|
+
|
|
934
|
+
| Command | Description |
|
|
935
|
+
|---------|-------------|
|
|
936
|
+
| `/grid:branch` | Show branch status |
|
|
937
|
+
| `/grid:branch create {name}` | Create feature branch |
|
|
938
|
+
| `/grid:branch switch {name}` | Switch branches |
|
|
939
|
+
| `/grid:branch pr` | Create PR |
|
|
940
|
+
| `/grid:branch cleanup` | Delete merged branches |
|
|
941
|
+
| `/grid:branch list` | List Grid branches |
|
|
942
|
+
| `/grid:branch sync` | Sync with main |
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
## Appendix B: Commit Message Examples
|
|
947
|
+
|
|
948
|
+
### Feature Commits
|
|
949
|
+
|
|
950
|
+
```
|
|
951
|
+
feat(block-01): implement user authentication
|
|
952
|
+
|
|
953
|
+
- Add JWT token generation with RS256
|
|
954
|
+
- Create login and logout endpoints
|
|
955
|
+
- Implement refresh token rotation
|
|
956
|
+
- Add password hashing with bcrypt
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Fix Commits
|
|
960
|
+
|
|
961
|
+
```
|
|
962
|
+
fix(api): handle null user response gracefully
|
|
963
|
+
|
|
964
|
+
- Add null check before accessing user properties
|
|
965
|
+
- Return 404 instead of 500 for missing users
|
|
966
|
+
- Add test case for null user scenario
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Refactor Commits
|
|
970
|
+
|
|
971
|
+
```
|
|
972
|
+
refactor(utils): simplify date formatting utilities
|
|
973
|
+
|
|
974
|
+
- Extract common date patterns to constants
|
|
975
|
+
- Replace moment.js with native Intl.DateTimeFormat
|
|
976
|
+
- Remove unused timezone conversion functions
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
---
|
|
980
|
+
|
|
981
|
+
*End of Technical Design Document*
|