2026-03-16 11:10:33 +00:00
// SPDX-License-Identifier: EUPL-1.2
// Package agentic provides MCP tools for agent orchestration.
// Prepares sandboxed workspaces and dispatches subagents.
package agentic
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
2026-03-16 21:48:31 +00:00
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
2026-03-16 11:10:33 +00:00
"github.com/modelcontextprotocol/go-sdk/mcp"
"gopkg.in/yaml.v3"
)
2026-03-17 17:45:04 +00:00
// CompletionNotifier is called when an agent completes, to trigger
// immediate notifications to connected clients.
type CompletionNotifier interface {
Poke ( )
}
2026-03-16 11:10:33 +00:00
// PrepSubsystem provides agentic MCP tools.
type PrepSubsystem struct {
forgeURL string
forgeToken string
brainURL string
brainKey string
specsPath string
codePath string
2026-03-17 17:45:04 +00:00
client * http . Client
onComplete CompletionNotifier
2026-03-16 11:10:33 +00:00
}
// NewPrep creates an agentic subsystem.
func NewPrep ( ) * PrepSubsystem {
home , _ := os . UserHomeDir ( )
forgeToken := os . Getenv ( "FORGE_TOKEN" )
if forgeToken == "" {
forgeToken = os . Getenv ( "GITEA_TOKEN" )
}
brainKey := os . Getenv ( "CORE_BRAIN_KEY" )
if brainKey == "" {
2026-03-16 21:48:31 +00:00
if data , err := coreio . Local . Read ( filepath . Join ( home , ".claude" , "brain.key" ) ) ; err == nil {
brainKey = strings . TrimSpace ( data )
2026-03-16 11:10:33 +00:00
}
}
return & PrepSubsystem {
2026-03-17 18:13:44 +00:00
forgeURL : envOr ( "FORGE_URL" , "https://forge.lthn.ai" ) ,
forgeToken : forgeToken ,
brainURL : envOr ( "CORE_BRAIN_URL" , "https://api.lthn.sh" ) ,
brainKey : brainKey ,
specsPath : envOr ( "SPECS_PATH" , filepath . Join ( home , "Code" , "specs" ) ) ,
codePath : envOr ( "CODE_PATH" , filepath . Join ( home , "Code" ) ) ,
client : & http . Client { Timeout : 30 * time . Second } ,
2026-03-16 11:10:33 +00:00
}
}
2026-03-17 17:45:04 +00:00
// SetCompletionNotifier wires up the monitor for immediate push on agent completion.
func ( s * PrepSubsystem ) SetCompletionNotifier ( n CompletionNotifier ) {
s . onComplete = n
}
2026-03-16 11:10:33 +00:00
func envOr ( key , fallback string ) string {
if v := os . Getenv ( key ) ; v != "" {
return v
}
return fallback
}
// Name implements mcp.Subsystem.
func ( s * PrepSubsystem ) Name ( ) string { return "agentic" }
// RegisterTools implements mcp.Subsystem.
func ( s * PrepSubsystem ) RegisterTools ( server * mcp . Server ) {
mcp . AddTool ( server , & mcp . Tool {
Name : "agentic_prep_workspace" ,
Description : "Prepare a sandboxed agent workspace with TODO.md, CLAUDE.md, CONTEXT.md, CONSUMERS.md, RECENT.md, and a git clone of the target repo in src/." ,
} , s . prepWorkspace )
s . registerDispatchTool ( server )
s . registerStatusTool ( server )
s . registerResumeTool ( server )
s . registerCreatePRTool ( server )
s . registerListPRsTool ( server )
s . registerEpicTool ( server )
2026-03-17 17:45:04 +00:00
s . registerMirrorTool ( server )
s . registerRemoteDispatchTool ( server )
s . registerRemoteStatusTool ( server )
s . registerReviewQueueTool ( server )
2026-03-16 11:10:33 +00:00
mcp . AddTool ( server , & mcp . Tool {
Name : "agentic_scan" ,
Description : "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug)." ,
} , s . scan )
s . registerPlanTools ( server )
2026-03-17 04:31:19 +00:00
s . registerWatchTool ( server )
2026-03-16 11:10:33 +00:00
}
// Shutdown implements mcp.SubsystemWithShutdown.
func ( s * PrepSubsystem ) Shutdown ( _ context . Context ) error { return nil }
// --- Input/Output types ---
// PrepInput is the input for agentic_prep_workspace.
type PrepInput struct {
Repo string ` json:"repo" ` // e.g. "go-io"
Org string ` json:"org,omitempty" ` // default "core"
Issue int ` json:"issue,omitempty" ` // Forge issue number
Task string ` json:"task,omitempty" ` // Task description (if no issue)
Template string ` json:"template,omitempty" ` // Prompt template: conventions, security, coding (default: coding)
PlanTemplate string ` json:"plan_template,omitempty" ` // Plan template slug: bug-fix, code-review, new-feature, refactor, feature-port
Variables map [ string ] string ` json:"variables,omitempty" ` // Template variable substitution
Persona string ` json:"persona,omitempty" ` // Persona slug: engineering/backend-architect, testing/api-tester, etc.
}
// PrepOutput is the output for agentic_prep_workspace.
type PrepOutput struct {
Success bool ` json:"success" `
WorkspaceDir string ` json:"workspace_dir" `
2026-03-17 04:19:48 +00:00
Branch string ` json:"branch" `
2026-03-16 11:10:33 +00:00
WikiPages int ` json:"wiki_pages" `
SpecFiles int ` json:"spec_files" `
Memories int ` json:"memories" `
Consumers int ` json:"consumers" `
ClaudeMd bool ` json:"claude_md" `
GitLog int ` json:"git_log_entries" `
}
func ( s * PrepSubsystem ) prepWorkspace ( ctx context . Context , _ * mcp . CallToolRequest , input PrepInput ) ( * mcp . CallToolResult , PrepOutput , error ) {
if input . Repo == "" {
2026-03-16 21:48:31 +00:00
return nil , PrepOutput { } , coreerr . E ( "prepWorkspace" , "repo is required" , nil )
2026-03-16 11:10:33 +00:00
}
if input . Org == "" {
input . Org = "core"
}
if input . Template == "" {
input . Template = "coding"
}
// Workspace root: .core/workspace/{repo}-{timestamp}/
2026-03-17 18:13:44 +00:00
wsRoot := WorkspaceRoot ( )
2026-03-16 11:10:33 +00:00
wsName := fmt . Sprintf ( "%s-%d" , input . Repo , time . Now ( ) . Unix ( ) )
wsDir := filepath . Join ( wsRoot , wsName )
// Create workspace structure
// kb/ and specs/ will be created inside src/ after clone
out := PrepOutput { WorkspaceDir : wsDir }
// Source repo path
repoPath := filepath . Join ( s . codePath , "core" , input . Repo )
// 1. Clone repo into src/ and create feature branch
srcDir := filepath . Join ( wsDir , "src" )
cloneCmd := exec . CommandContext ( ctx , "git" , "clone" , repoPath , srcDir )
2026-03-17 19:27:44 +00:00
if err := cloneCmd . Run ( ) ; err != nil {
return nil , PrepOutput { } , coreerr . E ( "prep" , "git clone failed for " + input . Repo , err )
}
2026-03-16 11:10:33 +00:00
// Create feature branch
taskSlug := strings . Map ( func ( r rune ) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' {
return r
}
if r >= 'A' && r <= 'Z' {
return r + 32 // lowercase
}
return '-'
} , input . Task )
if len ( taskSlug ) > 40 {
taskSlug = taskSlug [ : 40 ]
}
taskSlug = strings . Trim ( taskSlug , "-" )
branchName := fmt . Sprintf ( "agent/%s" , taskSlug )
branchCmd := exec . CommandContext ( ctx , "git" , "checkout" , "-b" , branchName )
branchCmd . Dir = srcDir
branchCmd . Run ( )
2026-03-17 04:19:48 +00:00
out . Branch = branchName
2026-03-16 11:10:33 +00:00
// Create context dirs inside src/
2026-03-16 21:48:31 +00:00
coreio . Local . EnsureDir ( filepath . Join ( srcDir , "kb" ) )
coreio . Local . EnsureDir ( filepath . Join ( srcDir , "specs" ) )
2026-03-16 11:10:33 +00:00
// Remote stays as local clone origin — agent cannot push to forge.
// Reviewer pulls changes from workspace and pushes after verification.
// 2. Copy CLAUDE.md and GEMINI.md to workspace
claudeMdPath := filepath . Join ( repoPath , "CLAUDE.md" )
2026-03-16 21:48:31 +00:00
if data , err := coreio . Local . Read ( claudeMdPath ) ; err == nil {
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "CLAUDE.md" ) , data )
2026-03-16 11:10:33 +00:00
out . ClaudeMd = true
}
// Copy GEMINI.md from core/agent (ethics framework for all agents)
agentGeminiMd := filepath . Join ( s . codePath , "core" , "agent" , "GEMINI.md" )
2026-03-16 21:48:31 +00:00
if data , err := coreio . Local . Read ( agentGeminiMd ) ; err == nil {
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "GEMINI.md" ) , data )
2026-03-16 11:10:33 +00:00
}
// Copy persona if specified
if input . Persona != "" {
personaPath := filepath . Join ( s . codePath , "core" , "agent" , "prompts" , "personas" , input . Persona + ".md" )
2026-03-16 21:48:31 +00:00
if data , err := coreio . Local . Read ( personaPath ) ; err == nil {
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "PERSONA.md" ) , data )
2026-03-16 11:10:33 +00:00
}
}
// 3. Generate TODO.md
if input . Issue > 0 {
s . generateTodo ( ctx , input . Org , input . Repo , input . Issue , wsDir )
} else if input . Task != "" {
todo := fmt . Sprintf ( "# TASK: %s\n\n**Repo:** %s/%s\n**Status:** ready\n\n## Objective\n\n%s\n" ,
input . Task , input . Org , input . Repo , input . Task )
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "TODO.md" ) , todo )
2026-03-16 11:10:33 +00:00
}
// 4. Generate CONTEXT.md from OpenBrain
out . Memories = s . generateContext ( ctx , input . Repo , wsDir )
// 5. Generate CONSUMERS.md
out . Consumers = s . findConsumers ( input . Repo , wsDir )
// 6. Generate RECENT.md
out . GitLog = s . gitLog ( repoPath , wsDir )
// 7. Pull wiki pages into kb/
out . WikiPages = s . pullWiki ( ctx , input . Org , input . Repo , wsDir )
// 8. Copy spec files into specs/
out . SpecFiles = s . copySpecs ( wsDir )
// 9. Write PLAN.md from template (if specified)
if input . PlanTemplate != "" {
s . writePlanFromTemplate ( input . PlanTemplate , input . Variables , input . Task , wsDir )
}
// 10. Write prompt template
s . writePromptTemplate ( input . Template , wsDir )
out . Success = true
return nil , out , nil
}
// --- Prompt templates ---
func ( s * PrepSubsystem ) writePromptTemplate ( template , wsDir string ) {
var prompt string
switch template {
case "conventions" :
2026-03-17 04:12:54 +00:00
prompt = ` # # SANDBOX : You are restricted to this directory only . No absolute paths , no cd . . , no editing outside src / .
Read CLAUDE . md for project conventions .
2026-03-16 11:10:33 +00:00
Review all Go files in src / for :
- Error handling : should use coreerr . E ( ) from go - log , not fmt . Errorf or errors . New
- Compile - time interface checks : var _ Interface = ( * Impl ) ( nil )
- Import aliasing : stdlib io aliased as goio
- UK English in comments ( colour not color , initialise not initialize )
- No fmt . Print * debug statements ( use go - log )
- Test coverage gaps
Report findings with file : line references . Do not fix — only report .
`
case "security" :
2026-03-17 04:12:54 +00:00
prompt = ` # # SANDBOX : You are restricted to this directory only . No absolute paths , no cd . . , no editing outside src / .
Read CLAUDE . md for project context .
2026-03-16 11:10:33 +00:00
Review all Go files in src / for security issues :
- Path traversal vulnerabilities
- Unvalidated input
- SQL injection ( if applicable )
- Hardcoded credentials or tokens
- Unsafe type assertions
- Missing error checks
- Race conditions ( shared state without mutex )
- Unsafe use of os / exec
Report findings with severity ( critical / high / medium / low ) and file : line references .
2026-03-17 17:45:04 +00:00
`
case "verify" :
prompt = ` Read PERSONA . md if it exists — adopt that identity and approach .
Read CLAUDE . md for project conventions and context .
You are verifying a pull request . The code in src / contains changes on a feature branch .
# # Your Tasks
1. * * Run tests * * : Execute the project ' s test suite ( go test . / ... , composer test , or npm test ) . Report results .
2. * * Review diff * * : Run ` + " ` git diff origin / main . . HEAD ` " + ` to see all changes . Review for :
- Correctness : Does the code do what the commit messages say ?
- Security : Path traversal , injection , hardcoded secrets , unsafe input handling
- Conventions : coreerr . E ( ) not fmt . Errorf , go - io not os . ReadFile , UK English
- Test coverage : Are new functions tested ?
3. * * Verdict * * : Write VERDICT . md with :
- PASS or FAIL ( first line , nothing else )
- Summary of findings ( if any )
- List of issues by severity ( critical / high / medium / low )
If PASS : the PR will be auto - merged .
If FAIL : your findings will be commented on the PR for the original agent to address .
Be strict but fair . A missing test is medium . A security issue is critical . A typo is low .
# # SANDBOX BOUNDARY ( HARD LIMIT )
You are restricted to the current directory and its subdirectories ONLY .
- Do NOT use absolute paths
- Do NOT navigate outside this repository
2026-03-16 11:10:33 +00:00
`
case "coding" :
prompt = ` Read PERSONA . md if it exists — adopt that identity and approach .
Read CLAUDE . md for project conventions and context .
Read TODO . md for your task .
Read PLAN . md if it exists — work through each phase in order .
Read CONTEXT . md for relevant knowledge from previous sessions .
Read CONSUMERS . md to understand breaking change risk .
Read RECENT . md for recent changes .
Work in the src / directory . Follow the conventions in CLAUDE . md .
2026-03-17 04:12:54 +00:00
# # SANDBOX BOUNDARY ( HARD LIMIT )
You are restricted to the current directory and its subdirectories ONLY .
- Do NOT use absolute paths ( e . g . , / Users / ... , / home / ... )
- Do NOT navigate with cd . . or cd /
- Do NOT edit files outside this repository
- Do NOT access parent directories or other repos
- Any path in Edit / Write tool calls MUST be relative to the current directory
Violation of these rules will cause your work to be rejected .
2026-03-16 11:10:33 +00:00
# # Workflow
If PLAN . md exists , you MUST work through it phase by phase :
1. Complete all tasks in the current phase
2. STOP and commit before moving on : type ( scope ) : phase N - description
3. Only then start the next phase
4. If you are blocked or unsure , write BLOCKED . md explaining the question and stop
5. Do NOT skip phases or combine multiple phases into one commit
Each phase = one commit . This is not optional .
If no PLAN . md , complete TODO . md as a single unit of work .
# # Commit Convention
Commit message format : type ( scope ) : description
Co - Author : Co - Authored - By : Virgil < virgil @ lethean . io >
Do NOT push . Commit only — a reviewer will verify and push .
`
default :
2026-03-17 04:12:54 +00:00
prompt = "SANDBOX: Restricted to this directory only. No absolute paths, no cd ..\n\nRead TODO.md and complete the task. Work in src/.\n"
2026-03-16 11:10:33 +00:00
}
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "PROMPT.md" ) , prompt )
2026-03-16 11:10:33 +00:00
}
// --- Plan template rendering ---
// writePlanFromTemplate loads a YAML plan template, substitutes variables,
// and writes PLAN.md into the workspace src/ directory.
func ( s * PrepSubsystem ) writePlanFromTemplate ( templateSlug string , variables map [ string ] string , task string , wsDir string ) {
// Look for template in core/agent/prompts/templates/
templatePath := filepath . Join ( s . codePath , "core" , "agent" , "prompts" , "templates" , templateSlug + ".yaml" )
2026-03-16 21:48:31 +00:00
data , err := coreio . Local . Read ( templatePath )
2026-03-16 11:10:33 +00:00
if err != nil {
// Try .yml extension
templatePath = filepath . Join ( s . codePath , "core" , "agent" , "prompts" , "templates" , templateSlug + ".yml" )
2026-03-16 21:48:31 +00:00
data , err = coreio . Local . Read ( templatePath )
2026-03-16 11:10:33 +00:00
if err != nil {
return // Template not found, skip silently
}
}
2026-03-16 21:48:31 +00:00
content := data
2026-03-16 11:10:33 +00:00
// Substitute variables ({{variable_name}} → value)
for key , value := range variables {
content = strings . ReplaceAll ( content , "{{" + key + "}}" , value )
content = strings . ReplaceAll ( content , "{{ " + key + " }}" , value )
}
// Parse the YAML to render as markdown
var tmpl struct {
Name string ` yaml:"name" `
Description string ` yaml:"description" `
Guidelines [ ] string ` yaml:"guidelines" `
Phases [ ] struct {
Name string ` yaml:"name" `
Description string ` yaml:"description" `
Tasks [ ] any ` yaml:"tasks" `
} ` yaml:"phases" `
}
if err := yaml . Unmarshal ( [ ] byte ( content ) , & tmpl ) ; err != nil {
return
}
// Render as PLAN.md
var plan strings . Builder
plan . WriteString ( "# Plan: " + tmpl . Name + "\n\n" )
if task != "" {
plan . WriteString ( "**Task:** " + task + "\n\n" )
}
if tmpl . Description != "" {
plan . WriteString ( tmpl . Description + "\n\n" )
}
if len ( tmpl . Guidelines ) > 0 {
plan . WriteString ( "## Guidelines\n\n" )
for _ , g := range tmpl . Guidelines {
plan . WriteString ( "- " + g + "\n" )
}
plan . WriteString ( "\n" )
}
for i , phase := range tmpl . Phases {
plan . WriteString ( fmt . Sprintf ( "## Phase %d: %s\n\n" , i + 1 , phase . Name ) )
if phase . Description != "" {
plan . WriteString ( phase . Description + "\n\n" )
}
for _ , task := range phase . Tasks {
switch t := task . ( type ) {
case string :
plan . WriteString ( "- [ ] " + t + "\n" )
case map [ string ] any :
if name , ok := t [ "name" ] . ( string ) ; ok {
plan . WriteString ( "- [ ] " + name + "\n" )
}
}
}
plan . WriteString ( "\n**Commit after completing this phase.**\n\n---\n\n" )
}
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "PLAN.md" ) , plan . String ( ) )
2026-03-16 11:10:33 +00:00
}
// --- Helpers (unchanged) ---
func ( s * PrepSubsystem ) pullWiki ( ctx context . Context , org , repo , wsDir string ) int {
if s . forgeToken == "" {
return 0
}
url := fmt . Sprintf ( "%s/api/v1/repos/%s/%s/wiki/pages" , s . forgeURL , org , repo )
req , _ := http . NewRequestWithContext ( ctx , "GET" , url , nil )
req . Header . Set ( "Authorization" , "token " + s . forgeToken )
resp , err := s . client . Do ( req )
2026-03-17 19:27:44 +00:00
if err != nil {
return 0
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
2026-03-16 11:10:33 +00:00
return 0
}
defer resp . Body . Close ( )
var pages [ ] struct {
Title string ` json:"title" `
SubURL string ` json:"sub_url" `
}
json . NewDecoder ( resp . Body ) . Decode ( & pages )
count := 0
for _ , page := range pages {
subURL := page . SubURL
if subURL == "" {
subURL = page . Title
}
pageURL := fmt . Sprintf ( "%s/api/v1/repos/%s/%s/wiki/page/%s" , s . forgeURL , org , repo , subURL )
pageReq , _ := http . NewRequestWithContext ( ctx , "GET" , pageURL , nil )
pageReq . Header . Set ( "Authorization" , "token " + s . forgeToken )
pageResp , err := s . client . Do ( pageReq )
if err != nil || pageResp . StatusCode != 200 {
continue
}
var pageData struct {
ContentBase64 string ` json:"content_base64" `
}
json . NewDecoder ( pageResp . Body ) . Decode ( & pageData )
pageResp . Body . Close ( )
if pageData . ContentBase64 == "" {
continue
}
content , _ := base64 . StdEncoding . DecodeString ( pageData . ContentBase64 )
filename := strings . Map ( func ( r rune ) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
return r
}
return '-'
} , page . Title ) + ".md"
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "kb" , filename ) , string ( content ) )
2026-03-16 11:10:33 +00:00
count ++
}
return count
}
func ( s * PrepSubsystem ) copySpecs ( wsDir string ) int {
specFiles := [ ] string { "AGENT_CONTEXT.md" , "TASK_PROTOCOL.md" }
count := 0
for _ , file := range specFiles {
src := filepath . Join ( s . specsPath , file )
2026-03-16 21:48:31 +00:00
if data , err := coreio . Local . Read ( src ) ; err == nil {
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "specs" , file ) , data )
2026-03-16 11:10:33 +00:00
count ++
}
}
return count
}
func ( s * PrepSubsystem ) generateContext ( ctx context . Context , repo , wsDir string ) int {
if s . brainKey == "" {
return 0
}
body , _ := json . Marshal ( map [ string ] any {
"query" : "architecture conventions key interfaces for " + repo ,
"top_k" : 10 ,
"project" : repo ,
"agent_id" : "cladius" ,
} )
req , _ := http . NewRequestWithContext ( ctx , "POST" , s . brainURL + "/v1/brain/recall" , strings . NewReader ( string ( body ) ) )
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Accept" , "application/json" )
req . Header . Set ( "Authorization" , "Bearer " + s . brainKey )
resp , err := s . client . Do ( req )
2026-03-17 19:27:44 +00:00
if err != nil {
return 0
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
2026-03-16 11:10:33 +00:00
return 0
}
defer resp . Body . Close ( )
respData , _ := io . ReadAll ( resp . Body )
var result struct {
Memories [ ] map [ string ] any ` json:"memories" `
}
json . Unmarshal ( respData , & result )
var content strings . Builder
content . WriteString ( "# Context — " + repo + "\n\n" )
content . WriteString ( "> Relevant knowledge from OpenBrain.\n\n" )
for i , mem := range result . Memories {
memType , _ := mem [ "type" ] . ( string )
memContent , _ := mem [ "content" ] . ( string )
memProject , _ := mem [ "project" ] . ( string )
score , _ := mem [ "score" ] . ( float64 )
content . WriteString ( fmt . Sprintf ( "### %d. %s [%s] (score: %.3f)\n\n%s\n\n" , i + 1 , memProject , memType , score , memContent ) )
}
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "CONTEXT.md" ) , content . String ( ) )
2026-03-16 11:10:33 +00:00
return len ( result . Memories )
}
func ( s * PrepSubsystem ) findConsumers ( repo , wsDir string ) int {
goWorkPath := filepath . Join ( s . codePath , "go.work" )
modulePath := "forge.lthn.ai/core/" + repo
2026-03-16 21:48:31 +00:00
workData , err := coreio . Local . Read ( goWorkPath )
2026-03-16 11:10:33 +00:00
if err != nil {
return 0
}
var consumers [ ] string
2026-03-16 21:48:31 +00:00
for _ , line := range strings . Split ( workData , "\n" ) {
2026-03-16 11:10:33 +00:00
line = strings . TrimSpace ( line )
if ! strings . HasPrefix ( line , "./" ) {
continue
}
dir := filepath . Join ( s . codePath , strings . TrimPrefix ( line , "./" ) )
goMod := filepath . Join ( dir , "go.mod" )
2026-03-16 21:48:31 +00:00
modData , err := coreio . Local . Read ( goMod )
2026-03-16 11:10:33 +00:00
if err != nil {
continue
}
2026-03-16 21:48:31 +00:00
if strings . Contains ( modData , modulePath ) && ! strings . HasPrefix ( modData , "module " + modulePath ) {
2026-03-16 11:10:33 +00:00
consumers = append ( consumers , filepath . Base ( dir ) )
}
}
if len ( consumers ) > 0 {
content := "# Consumers of " + repo + "\n\n"
content += "These modules import `" + modulePath + "`:\n\n"
for _ , c := range consumers {
content += "- " + c + "\n"
}
content += fmt . Sprintf ( "\n**Breaking change risk: %d consumers.**\n" , len ( consumers ) )
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "CONSUMERS.md" ) , content )
2026-03-16 11:10:33 +00:00
}
return len ( consumers )
}
func ( s * PrepSubsystem ) gitLog ( repoPath , wsDir string ) int {
cmd := exec . Command ( "git" , "log" , "--oneline" , "-20" )
cmd . Dir = repoPath
output , err := cmd . Output ( )
if err != nil {
return 0
}
lines := strings . Split ( strings . TrimSpace ( string ( output ) ) , "\n" )
if len ( lines ) > 0 && lines [ 0 ] != "" {
content := "# Recent Changes\n\n```\n" + string ( output ) + "```\n"
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "RECENT.md" ) , content )
2026-03-16 11:10:33 +00:00
}
return len ( lines )
}
func ( s * PrepSubsystem ) generateTodo ( ctx context . Context , org , repo string , issue int , wsDir string ) {
if s . forgeToken == "" {
return
}
url := fmt . Sprintf ( "%s/api/v1/repos/%s/%s/issues/%d" , s . forgeURL , org , repo , issue )
req , _ := http . NewRequestWithContext ( ctx , "GET" , url , nil )
req . Header . Set ( "Authorization" , "token " + s . forgeToken )
resp , err := s . client . Do ( req )
2026-03-17 19:27:44 +00:00
if err != nil {
return
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
2026-03-16 11:10:33 +00:00
return
}
defer resp . Body . Close ( )
var issueData struct {
Title string ` json:"title" `
Body string ` json:"body" `
}
json . NewDecoder ( resp . Body ) . Decode ( & issueData )
content := fmt . Sprintf ( "# TASK: %s\n\n" , issueData . Title )
content += fmt . Sprintf ( "**Status:** ready\n" )
content += fmt . Sprintf ( "**Source:** %s/%s/%s/issues/%d\n" , s . forgeURL , org , repo , issue )
content += fmt . Sprintf ( "**Repo:** %s/%s\n\n---\n\n" , org , repo )
content += "## Objective\n\n" + issueData . Body + "\n"
2026-03-16 21:48:31 +00:00
coreio . Local . Write ( filepath . Join ( wsDir , "src" , "TODO.md" ) , content )
2026-03-16 11:10:33 +00:00
}