Skip to content
Draft
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
48 changes: 27 additions & 21 deletions pkg/codingcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/hashicorp/go-getter/v2"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/markdown"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/memoryusage"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/selectors"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/skills"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/taskparser"
Expand Down Expand Up @@ -52,25 +53,25 @@ const (

// Context holds the configuration and state for assembling coding context.
type Context struct {
params taskparser.Params
includes selectors.Selectors
manifestURL string
searchPaths []string
downloadedPaths []string
task markdown.Markdown[markdown.TaskFrontMatter] // Parsed task
rules []markdown.Markdown[markdown.RuleFrontMatter] // Collected rule files
skills skills.AvailableSkills // Discovered skills (metadata only)
totalTokens int
logger *slog.Logger
cmdRunner func(cmd *exec.Cmd) error
params taskparser.Params
includes selectors.Selectors
manifestURL string
searchPaths []string
downloadedPaths []string
task markdown.Markdown[markdown.TaskFrontMatter] // Parsed task
rules []markdown.Markdown[markdown.RuleFrontMatter] // Collected rule files
skills skills.AvailableSkills // Discovered skills (metadata only)
totalTokens int
logger *slog.Logger
cmdRunner func(cmd *exec.Cmd) error
resume bool
doBootstrap bool // Controls whether to discover rules, skills, and run bootstrap scripts
includeByDefault bool // Controls whether unmatched rules/skills are included by default
agent Agent
namespace string // Active namespace derived from task name (e.g. "myteam" from "myteam/fix-bug")
userPrompt string // User-provided prompt to append to task
lintMode bool
lintCollector *lintCollector
agent Agent
namespace string // Active namespace derived from task name (e.g. "myteam" from "myteam/fix-bug")
userPrompt string // User-provided prompt to append to task
lintMode bool
lintCollector *lintCollector
}

// parseNamespacedTaskName splits a task name into its optional namespace and base name.
Expand Down Expand Up @@ -102,11 +103,11 @@ func parseNamespacedTaskName(taskName string) (string, string, error) {
// New creates a new Context with the given options.
func New(opts ...Option) *Context {
c := &Context{
params: make(taskparser.Params),
includes: make(selectors.Selectors),
rules: make([]markdown.Markdown[markdown.RuleFrontMatter], 0),
skills: skills.AvailableSkills{Skills: make([]skills.Skill, 0)},
logger: slog.New(slog.NewTextHandler(os.Stderr, nil)),
params: make(taskparser.Params),
includes: make(selectors.Selectors),
rules: make([]markdown.Markdown[markdown.RuleFrontMatter], 0),
skills: skills.AvailableSkills{Skills: make([]skills.Skill, 0)},
logger: slog.New(slog.NewTextHandler(os.Stderr, nil)),
doBootstrap: true, // Default to true for backward compatibility
includeByDefault: true, // Default to true for backward compatibility
cmdRunner: func(cmd *exec.Cmd) error {
Expand Down Expand Up @@ -175,6 +176,11 @@ func (cc *Context) Run(ctx context.Context, taskName string) (*Result, error) {
// Estimate tokens for task
cc.logger.Info("Total estimated tokens", "tokens", cc.totalTokens)

// Log current cgroup v2 memory usage if available (e.g. in containerized environments)
if memBytes, err := memoryusage.ReadCurrent(); err == nil {
cc.logger.Info("Current memory usage", "bytes", memBytes)
}

// Build the combined prompt from all rules and task content
var promptBuilder strings.Builder
for _, rule := range cc.rules {
Expand Down
1 change: 0 additions & 1 deletion pkg/codingcontext/enumerate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,3 @@ func TestDiscoveredTask_Fields(t *testing.T) {
t.Errorf("namespaced Path = %q, want *ns-task.md", namespaced.Path)
}
}

35 changes: 35 additions & 0 deletions pkg/codingcontext/memoryusage/memoryusage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package memoryusage provides memory usage reading from cgroup v2.
package memoryusage

import (
"fmt"
"os"
"strconv"
"strings"
)

const cgroupV2MemoryCurrentPath = "/sys/fs/cgroup/memory.current"

// ReadCurrent reads the current memory usage in bytes from the cgroup v2
// memory.current file at the default path. Returns an error if the file
// cannot be read or parsed.
func ReadCurrent() (int64, error) {
return ReadCurrentFromPath(cgroupV2MemoryCurrentPath)
}

// ReadCurrentFromPath reads the current memory usage in bytes from the
// provided cgroup v2 memory.current file path. Returns an error if the
// file cannot be read or parsed.
func ReadCurrentFromPath(path string) (int64, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, fmt.Errorf("reading %s: %w", path, err)
}

val, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
if err != nil {
return 0, fmt.Errorf("parsing %s: %w", path, err)
}

return val, nil
}
95 changes: 95 additions & 0 deletions pkg/codingcontext/memoryusage/memoryusage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package memoryusage_test

import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/memoryusage"
)

func TestReadCurrent(t *testing.T) {
t.Parallel()

tests := []struct {
name string
fileContent string
wantBytes int64
wantErr bool
}{
{
name: "valid memory value",
fileContent: "12345678\n",
wantBytes: 12345678,
},
{
name: "valid value without newline",
fileContent: "999999",
wantBytes: 999999,
},
{
name: "zero value",
fileContent: "0\n",
wantBytes: 0,
},
{
name: "invalid non-numeric content",
fileContent: "abc\n",
wantErr: true,
},
{
name: "empty content",
fileContent: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

tmpDir := t.TempDir()
memFile := filepath.Join(tmpDir, "memory.current")

if err := os.WriteFile(memFile, []byte(tt.fileContent), 0o600); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}

got, err := memoryusage.ReadCurrentFromPath(memFile)
if (err != nil) != tt.wantErr {
t.Errorf("ReadCurrentFromPath() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && got != tt.wantBytes {
t.Errorf("ReadCurrentFromPath() = %d, want %d", got, tt.wantBytes)
}
})
}
}

func TestReadCurrent_FileNotFound(t *testing.T) {
t.Parallel()

_, err := memoryusage.ReadCurrentFromPath("/nonexistent/path/memory.current")
if err == nil {
t.Error("ReadCurrentFromPath() expected error for missing file, got nil")
}
}

func TestReadCurrent_LiveCgroup(t *testing.T) {
t.Parallel()

bytes, err := memoryusage.ReadCurrent()
if err != nil {
// On systems without cgroup v2 memory.current this is expected.
t.Skipf("cgroup v2 memory.current not available: %v", err)
}

if bytes < 0 {
t.Errorf("ReadCurrent() returned negative value %d", bytes)
}

t.Logf("current cgroup memory usage: %s bytes", strconv.FormatInt(bytes, 10))
}