go: implement GCCWorkspace with full operation set
Swadhin Biswas committed 7 months ago
6011ef8
1 parent e26b07b
commit 6011ef8
1 file changed
+480 -0
Showing 1 changed file+480-0
▼GO/cortexa/workspace.go
+480-0
| @@ -0,0 +1,480 @@ | ||
1 | +package cortexa | |
2 | + | |
3 | +import ( | |
4 | + "crypto/rand" | |
5 | + "encoding/hex" | |
6 | + "fmt" | |
7 | + "os" | |
8 | + "path/filepath" | |
9 | + "strings" | |
10 | + "time" | |
11 | + | |
12 | + "gopkg.in/yaml.v3" | |
13 | +) | |
14 | + | |
15 | +const ( | |
16 | + mainBranch = "main" | |
17 | + gccDir = ".GCC" | |
18 | +) | |
19 | + | |
20 | +func nowISO() string { | |
21 | + return time.Now().UTC().Format(time.RFC3339) | |
22 | +} | |
23 | + | |
24 | +func shortID() string { | |
25 | + b := make([]byte, 4) | |
26 | + rand.Read(b) | |
27 | + return hex.EncodeToString(b) | |
28 | +} | |
29 | + | |
30 | +// Workspace manages the .GCC/ directory for one agent project. | |
31 | +// | |
32 | +// Implements the four GCC commands (arXiv:2508.00031v2): | |
33 | +// | |
34 | +// COMMIT (§3.2) — milestone checkpointing to commit.md | |
35 | +// BRANCH (§3.3) — isolated reasoning workspace | |
36 | +// MERGE (§3.4) — synthesise divergent paths | |
37 | +// CONTEXT (§3.5) — hierarchical memory retrieval (K-commit window) | |
38 | +// | |
39 | +// File system layout: | |
40 | +// | |
41 | +// .GCC/ | |
42 | +// ├── main.md # Global roadmap / planning artifact | |
43 | +// └── branches/ | |
44 | +// ├── main/ | |
45 | +// │ ├── log.md # Continuous OTA trace | |
46 | +// │ ├── commit.md # Milestone-level summaries | |
47 | +// │ └── metadata.yaml # Branch intent & status | |
48 | +// └── <branch>/ | |
49 | +// └── ... | |
50 | +type Workspace struct { | |
51 | + root string | |
52 | + gccPath string | |
53 | + currentBranch string | |
54 | +} | |
55 | + | |
56 | +// New returns a Workspace rooted at projectRoot. | |
57 | +func New(projectRoot string) *Workspace { | |
58 | + return &Workspace{ | |
59 | + root: projectRoot, | |
60 | + gccPath: filepath.Join(projectRoot, gccDir), | |
61 | + currentBranch: mainBranch, | |
62 | + } | |
63 | +} | |
64 | + | |
65 | +// ------------------------------------------------------------------ // | |
66 | +// Paths // | |
67 | +// ------------------------------------------------------------------ // | |
68 | + | |
69 | +func (w *Workspace) branchDir(branch string) string { | |
70 | + return filepath.Join(w.gccPath, "branches", branch) | |
71 | +} | |
72 | +func (w *Workspace) logPath(branch string) string { | |
73 | + return filepath.Join(w.branchDir(branch), "log.md") | |
74 | +} | |
75 | +func (w *Workspace) commitPath(branch string) string { | |
76 | + return filepath.Join(w.branchDir(branch), "commit.md") | |
77 | +} | |
78 | +func (w *Workspace) metaPath(branch string) string { | |
79 | + return filepath.Join(w.branchDir(branch), "metadata.yaml") | |
80 | +} | |
81 | +func (w *Workspace) mainMd() string { | |
82 | + return filepath.Join(w.gccPath, "main.md") | |
83 | +} | |
84 | + | |
85 | +// ------------------------------------------------------------------ // | |
86 | +// I/O helpers // | |
87 | +// ------------------------------------------------------------------ // | |
88 | + | |
89 | +func (w *Workspace) read(path string) string { | |
90 | + data, err := os.ReadFile(path) | |
91 | + if err != nil { | |
92 | + return "" | |
93 | + } | |
94 | + return string(data) | |
95 | +} | |
96 | + | |
97 | +func (w *Workspace) write(path, content string) error { | |
98 | + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | |
99 | + return err | |
100 | + } | |
101 | + return os.WriteFile(path, []byte(content), 0o644) | |
102 | +} | |
103 | + | |
104 | +func (w *Workspace) appendFile(path, content string) error { | |
105 | + f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | |
106 | + if err != nil { | |
107 | + return err | |
108 | + } | |
109 | + defer f.Close() | |
110 | + _, err = f.WriteString(content) | |
111 | + return err | |
112 | +} | |
113 | + | |
114 | +func (w *Workspace) parseCommits(branch string) []CommitRecord { | |
115 | + text := w.read(w.commitPath(branch)) | |
116 | + var records []CommitRecord | |
117 | + for _, block := range strings.Split(text, "---\n") { | |
118 | + block = strings.TrimSpace(block) | |
119 | + if block == "" { | |
120 | + continue | |
121 | + } | |
122 | + var ( | |
123 | + commitID, branchPurpose, prevSummary, contribution, ts string | |
124 | + ) | |
125 | + for _, line := range strings.Split(block, "\n") { | |
126 | + switch { | |
127 | + case strings.HasPrefix(line, "## Commit `"): | |
128 | + commitID = strings.TrimSuffix(strings.TrimPrefix(line, "## Commit `"), "`") | |
129 | + case strings.HasPrefix(line, "**Timestamp:**"): | |
130 | + ts = strings.TrimSpace(strings.TrimPrefix(line, "**Timestamp:**")) | |
131 | + case strings.HasPrefix(line, "**Branch Purpose:**"): | |
132 | + branchPurpose = strings.TrimSpace(strings.TrimPrefix(line, "**Branch Purpose:**")) | |
133 | + case strings.HasPrefix(line, "**Previous Progress Summary:**"): | |
134 | + prevSummary = strings.TrimSpace(strings.TrimPrefix(line, "**Previous Progress Summary:**")) | |
135 | + case strings.HasPrefix(line, "**This Commit's Contribution:**"): | |
136 | + contribution = strings.TrimSpace(strings.TrimPrefix(line, "**This Commit's Contribution:**")) | |
137 | + } | |
138 | + } | |
139 | + if commitID != "" { | |
140 | + records = append(records, CommitRecord{ | |
141 | + CommitID: commitID, | |
142 | + BranchName: branch, | |
143 | + BranchPurpose: branchPurpose, | |
144 | + PreviousProgressSummary: prevSummary, | |
145 | + ThisCommitContribution: contribution, | |
146 | + Timestamp: ts, | |
147 | + }) | |
148 | + } | |
149 | + } | |
150 | + return records | |
151 | +} | |
152 | + | |
153 | +func (w *Workspace) parseOTA(branch string) []OTARecord { | |
154 | + text := w.read(w.logPath(branch)) | |
155 | + var records []OTARecord | |
156 | + for _, block := range strings.Split(text, "---\n") { | |
157 | + block = strings.TrimSpace(block) | |
158 | + if block == "" { | |
159 | + continue | |
160 | + } | |
161 | + var step int | |
162 | + var ts, obs, thought, action string | |
163 | + for _, line := range strings.Split(block, "\n") { | |
164 | + switch { | |
165 | + case strings.HasPrefix(line, "### Step "): | |
166 | + parts := strings.SplitN(line, " — ", 2) | |
167 | + fmt.Sscanf(parts[0], "### Step %d", &step) | |
168 | + if len(parts) > 1 { | |
169 | + ts = strings.TrimSpace(parts[1]) | |
170 | + } | |
171 | + case strings.HasPrefix(line, "**Observation:**"): | |
172 | + obs = strings.TrimSpace(strings.TrimPrefix(line, "**Observation:**")) | |
173 | + case strings.HasPrefix(line, "**Thought:**"): | |
174 | + thought = strings.TrimSpace(strings.TrimPrefix(line, "**Thought:**")) | |
175 | + case strings.HasPrefix(line, "**Action:**"): | |
176 | + action = strings.TrimSpace(strings.TrimPrefix(line, "**Action:**")) | |
177 | + } | |
178 | + } | |
179 | + if obs != "" || thought != "" { | |
180 | + records = append(records, OTARecord{ | |
181 | + Step: step, | |
182 | + Timestamp: ts, | |
183 | + Observation: obs, | |
184 | + Thought: thought, | |
185 | + Action: action, | |
186 | + }) | |
187 | + } | |
188 | + } | |
189 | + return records | |
190 | +} | |
191 | + | |
192 | +func (w *Workspace) parseMeta(branch string) (*BranchMetadata, error) { | |
193 | + text := w.read(w.metaPath(branch)) | |
194 | + if text == "" { | |
195 | + return nil, nil | |
196 | + } | |
197 | + var meta BranchMetadata | |
198 | + if err := yaml.Unmarshal([]byte(text), &meta); err != nil { | |
199 | + return nil, err | |
200 | + } | |
201 | + return &meta, nil | |
202 | +} | |
203 | + | |
204 | +// ------------------------------------------------------------------ // | |
205 | +// Initialisation // | |
206 | +// ------------------------------------------------------------------ // | |
207 | + | |
208 | +// Init initialises a new GCC workspace. | |
209 | +func (w *Workspace) Init(projectRoadmap string) error { | |
210 | + if _, err := os.Stat(w.gccPath); err == nil { | |
211 | + return fmt.Errorf("GCC workspace already exists at %s", w.gccPath) | |
212 | + } | |
213 | + if err := os.MkdirAll(w.branchDir(mainBranch), 0o755); err != nil { | |
214 | + return err | |
215 | + } | |
216 | + | |
217 | + roadmap := fmt.Sprintf("# Project Roadmap\n\n**Initialized:** %s\n\n%s\n", nowISO(), projectRoadmap) | |
218 | + if err := w.write(w.mainMd(), roadmap); err != nil { | |
219 | + return err | |
220 | + } | |
221 | + if err := w.write(w.logPath(mainBranch), fmt.Sprintf("# OTA Log — branch `%s`\n\n", mainBranch)); err != nil { | |
222 | + return err | |
223 | + } | |
224 | + if err := w.write(w.commitPath(mainBranch), fmt.Sprintf("# Commit History — branch `%s`\n\n", mainBranch)); err != nil { | |
225 | + return err | |
226 | + } | |
227 | + | |
228 | + meta := BranchMetadata{ | |
229 | + Name: mainBranch, | |
230 | + Purpose: "Primary reasoning trajectory", | |
231 | + CreatedFrom: "", | |
232 | + CreatedAt: nowISO(), | |
233 | + Status: "active", | |
234 | + } | |
235 | + data, err := yaml.Marshal(meta) | |
236 | + if err != nil { | |
237 | + return err | |
238 | + } | |
239 | + if err := w.write(w.metaPath(mainBranch), string(data)); err != nil { | |
240 | + return err | |
241 | + } | |
242 | + | |
243 | + w.currentBranch = mainBranch | |
244 | + return nil | |
245 | +} | |
246 | + | |
247 | +// Load opens an existing GCC workspace. | |
248 | +func (w *Workspace) Load() error { | |
249 | + if _, err := os.Stat(w.gccPath); os.IsNotExist(err) { | |
250 | + return fmt.Errorf("no GCC workspace found at %s", w.gccPath) | |
251 | + } | |
252 | + w.currentBranch = mainBranch | |
253 | + return nil | |
254 | +} | |
255 | + | |
256 | +// ------------------------------------------------------------------ // | |
257 | +// GCC Commands // | |
258 | +// ------------------------------------------------------------------ // | |
259 | + | |
260 | +// LogOTA appends an Observation–Thought–Action step to the current branch's log.md. | |
261 | +func (w *Workspace) LogOTA(observation, thought, action string) (OTARecord, error) { | |
262 | + existing := w.parseOTA(w.currentBranch) | |
263 | + record := OTARecord{ | |
264 | + Step: len(existing) + 1, | |
265 | + Timestamp: nowISO(), | |
266 | + Observation: observation, | |
267 | + Thought: thought, | |
268 | + Action: action, | |
269 | + } | |
270 | + if err := w.appendFile(w.logPath(w.currentBranch), record.ToMarkdown()); err != nil { | |
271 | + return OTARecord{}, err | |
272 | + } | |
273 | + return record, nil | |
274 | +} | |
275 | + | |
276 | +// Commit is the COMMIT command (paper §3.2). | |
277 | +// Checkpoints milestone with: Branch Purpose, Previous Progress Summary, | |
278 | +// This Commit's Contribution. | |
279 | +func (w *Workspace) Commit(contribution string, previousSummary *string, updateRoadmap *string) (CommitRecord, error) { | |
280 | + meta, _ := w.parseMeta(w.currentBranch) | |
281 | + branchPurpose := "" | |
282 | + if meta != nil { | |
283 | + branchPurpose = meta.Purpose | |
284 | + } | |
285 | + | |
286 | + prevSummary := "Initial state — no prior commits." | |
287 | + if previousSummary != nil { | |
288 | + prevSummary = *previousSummary | |
289 | + } else { | |
290 | + commits := w.parseCommits(w.currentBranch) | |
291 | + if len(commits) > 0 { | |
292 | + prevSummary = commits[len(commits)-1].ThisCommitContribution | |
293 | + } | |
294 | + } | |
295 | + | |
296 | + record := CommitRecord{ | |
297 | + CommitID: shortID(), | |
298 | + BranchName: w.currentBranch, | |
299 | + BranchPurpose: branchPurpose, | |
300 | + PreviousProgressSummary: prevSummary, | |
301 | + ThisCommitContribution: contribution, | |
302 | + Timestamp: nowISO(), | |
303 | + } | |
304 | + | |
305 | + if err := w.appendFile(w.commitPath(w.currentBranch), record.ToMarkdown()); err != nil { | |
306 | + return CommitRecord{}, err | |
307 | + } | |
308 | + if updateRoadmap != nil { | |
309 | + update := fmt.Sprintf("\n## Update (%s)\n%s\n", record.Timestamp, *updateRoadmap) | |
310 | + if err := w.appendFile(w.mainMd(), update); err != nil { | |
311 | + return CommitRecord{}, err | |
312 | + } | |
313 | + } | |
314 | + return record, nil | |
315 | +} | |
316 | + | |
317 | +// Branch is the BRANCH command (paper §3.3). | |
318 | +// Creates isolated workspace: B_t^(name) = BRANCH(M_{t-1}). | |
319 | +func (w *Workspace) Branch(name, purpose string) error { | |
320 | + if _, err := os.Stat(w.branchDir(name)); err == nil { | |
321 | + return fmt.Errorf("branch '%s' already exists", name) | |
322 | + } | |
323 | + if err := os.MkdirAll(w.branchDir(name), 0o755); err != nil { | |
324 | + return err | |
325 | + } | |
326 | + if err := w.write(w.logPath(name), fmt.Sprintf("# OTA Log — branch `%s`\n\n", name)); err != nil { | |
327 | + return err | |
328 | + } | |
329 | + if err := w.write(w.commitPath(name), fmt.Sprintf("# Commit History — branch `%s`\n\n", name)); err != nil { | |
330 | + return err | |
331 | + } | |
332 | + meta := BranchMetadata{ | |
333 | + Name: name, | |
334 | + Purpose: purpose, | |
335 | + CreatedFrom: w.currentBranch, | |
336 | + CreatedAt: nowISO(), | |
337 | + Status: "active", | |
338 | + } | |
339 | + data, err := yaml.Marshal(meta) | |
340 | + if err != nil { | |
341 | + return err | |
342 | + } | |
343 | + if err := w.write(w.metaPath(name), string(data)); err != nil { | |
344 | + return err | |
345 | + } | |
346 | + w.currentBranch = name | |
347 | + return nil | |
348 | +} | |
349 | + | |
350 | +// Merge is the MERGE command (paper §3.4). | |
351 | +// Integrates a completed branch into target, merging summaries and OTA traces. | |
352 | +func (w *Workspace) Merge(branchName string, summary *string, target string) (CommitRecord, error) { | |
353 | + if _, err := os.Stat(w.branchDir(branchName)); os.IsNotExist(err) { | |
354 | + return CommitRecord{}, fmt.Errorf("branch '%s' not found", branchName) | |
355 | + } | |
356 | + if _, err := os.Stat(w.branchDir(target)); os.IsNotExist(err) { | |
357 | + return CommitRecord{}, fmt.Errorf("target branch '%s' not found", target) | |
358 | + } | |
359 | + | |
360 | + branchCommits := w.parseCommits(branchName) | |
361 | + branchOTA := w.parseOTA(branchName) | |
362 | + meta, _ := w.parseMeta(branchName) | |
363 | + | |
364 | + var mergeSummary string | |
365 | + if summary != nil { | |
366 | + mergeSummary = *summary | |
367 | + } else { | |
368 | + contribs := make([]string, len(branchCommits)) | |
369 | + for i, c := range branchCommits { | |
370 | + contribs[i] = c.ThisCommitContribution | |
371 | + } | |
372 | + mergeSummary = fmt.Sprintf("Merged branch `%s` (%d commits). Contributions: %s", | |
373 | + branchName, len(branchCommits), strings.Join(contribs, " | ")) | |
374 | + } | |
375 | + | |
376 | + if len(branchOTA) > 0 { | |
377 | + header := fmt.Sprintf("\n## Merged from `%s` (%s)\n\n", branchName, nowISO()) | |
378 | + if err := w.appendFile(w.logPath(target), header); err != nil { | |
379 | + return CommitRecord{}, err | |
380 | + } | |
381 | + for _, rec := range branchOTA { | |
382 | + if err := w.appendFile(w.logPath(target), rec.ToMarkdown()); err != nil { | |
383 | + return CommitRecord{}, err | |
384 | + } | |
385 | + } | |
386 | + } | |
387 | + | |
388 | + w.currentBranch = target | |
389 | + metaPurpose := "" | |
390 | + if meta != nil { | |
391 | + metaPurpose = meta.Purpose | |
392 | + } | |
393 | + prevSummary := fmt.Sprintf("Merging branch `%s` with purpose: %s", branchName, metaPurpose) | |
394 | + mergeCommit, err := w.Commit(mergeSummary, &prevSummary, &mergeSummary) | |
395 | + if err != nil { | |
396 | + return CommitRecord{}, err | |
397 | + } | |
398 | + | |
399 | + if meta != nil { | |
400 | + meta.Status = "merged" | |
401 | + merged := target | |
402 | + meta.MergedInto = &merged | |
403 | + mergedAt := nowISO() | |
404 | + meta.MergedAt = &mergedAt | |
405 | + data, err := yaml.Marshal(meta) | |
406 | + if err != nil { | |
407 | + return CommitRecord{}, err | |
408 | + } | |
409 | + if err := w.write(w.metaPath(branchName), string(data)); err != nil { | |
410 | + return CommitRecord{}, err | |
411 | + } | |
412 | + } | |
413 | + | |
414 | + return mergeCommit, nil | |
415 | +} | |
416 | + | |
417 | +// Context is the CONTEXT command (paper §3.5). | |
418 | +// Retrieves history at K-commit resolution. Paper default: K=1. | |
419 | +func (w *Workspace) Context(branch *string, k int) (ContextResult, error) { | |
420 | + target := w.currentBranch | |
421 | + if branch != nil { | |
422 | + target = *branch | |
423 | + } | |
424 | + if _, err := os.Stat(w.branchDir(target)); os.IsNotExist(err) { | |
425 | + return ContextResult{}, fmt.Errorf("branch '%s' not found", target) | |
426 | + } | |
427 | + | |
428 | + allCommits := w.parseCommits(target) | |
429 | + start := len(allCommits) - k | |
430 | + if start < 0 { | |
431 | + start = 0 | |
432 | + } | |
433 | + commits := allCommits[start:] | |
434 | + otaRecords := w.parseOTA(target) | |
435 | + mainRoadmap := w.read(w.mainMd()) | |
436 | + meta, _ := w.parseMeta(target) | |
437 | + | |
438 | + return ContextResult{ | |
439 | + BranchName: target, | |
440 | + K: k, | |
441 | + Commits: commits, | |
442 | + OTARecords: otaRecords, | |
443 | + MainRoadmap: mainRoadmap, | |
444 | + Metadata: meta, | |
445 | + }, nil | |
446 | +} | |
447 | + | |
448 | +// ------------------------------------------------------------------ // | |
449 | +// Helpers // | |
450 | +// ------------------------------------------------------------------ // | |
451 | + | |
452 | +// CurrentBranch returns the active branch name. | |
453 | +func (w *Workspace) CurrentBranch() string { | |
454 | + return w.currentBranch | |
455 | +} | |
456 | + | |
457 | +// SwitchBranch changes the active branch without creating a new one. | |
458 | +func (w *Workspace) SwitchBranch(name string) error { | |
459 | + if _, err := os.Stat(w.branchDir(name)); os.IsNotExist(err) { | |
460 | + return fmt.Errorf("branch '%s' does not exist", name) | |
461 | + } | |
462 | + w.currentBranch = name | |
463 | + return nil | |
464 | +} | |
465 | + | |
466 | +// ListBranches returns all branch names in the workspace. | |
467 | +func (w *Workspace) ListBranches() ([]string, error) { | |
468 | + branchesRoot := filepath.Join(w.gccPath, "branches") | |
469 | + entries, err := os.ReadDir(branchesRoot) | |
470 | + if err != nil { | |
471 | + return nil, err | |
472 | + } | |
473 | + var branches []string | |
474 | + for _, e := range entries { | |
475 | + if e.IsDir() { | |
476 | + branches = append(branches, e.Name()) | |
477 | + } | |
478 | + } | |
479 | + return branches, nil | |
480 | +} | |
0 | 481 | |