← back to johnjansen__anvil

Function bodies 169 total

All specs Real LLM only Function bodies
daemon.Daemon.handleStartTask method · go · L1329-L1366 (38 LOC)
internal/daemon/daemon.go
func (d *Daemon) handleStartTask(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req StartRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}
	if req.TaskID == "" {
		http.Error(w, "task_id is required", http.StatusBadRequest)
		return
	}

	// Clear stopped state
	d.stoppedMu.Lock()
	wasStopped := d.stoppedTasks[req.TaskID]
	delete(d.stoppedTasks, req.TaskID)
	d.stoppedMu.Unlock()

	// Also clear per-task drain state
	d.drainedMu.Lock()
	delete(d.drainedTasks, req.TaskID)
	d.drainedMu.Unlock()

	if !wasStopped {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"status": "already_running", "id": req.TaskID})
		return
	}

	dlog.Info("persistent task %s started — will be dispatched on next tick", req.TaskName)

	w.Header().Set("Content-Type", "a
daemon.Daemon.pruneOldData method · go · L1726-L1741 (16 LOC)
internal/daemon/daemon.go
func (d *Daemon) pruneOldData(now time.Time) {
	paths := loadWatchedPaths()
	if len(paths) == 0 {
		return
	}

	// Only prune on cron ticks (once per minute) to avoid overhead
	thisMinute := now.Truncate(time.Minute)
	if !thisMinute.Equal(d.lastTick) {
		return
	}

	for _, projPath := range paths {
		d.pruneProject(projPath)
	}
}
daemon.Daemon.pruneProject method · go · L1743-L1757 (15 LOC)
internal/daemon/daemon.go
func (d *Daemon) pruneProject(projPath string) {
	anvilDir := filepath.Join(projPath, ".anvil")

	// Prune logs
	logsDir := filepath.Join(anvilDir, "logs")
	if _, err := os.Stat(logsDir); err == nil {
		d.pruneDir(logsDir, "log")
	}

	// Prune runs
	runsDir := filepath.Join(anvilDir, "runs")
	if _, err := os.Stat(runsDir); err == nil {
		d.pruneDir(runsDir, "run")
	}
}
daemon.Daemon.pruneDir method · go · L1759-L1772 (14 LOC)
internal/daemon/daemon.go
func (d *Daemon) pruneDir(dir, kind string) {
	taskDirs, err := os.ReadDir(dir)
	if err != nil {
		return
	}

	for _, taskDir := range taskDirs {
		if !taskDir.IsDir() {
			continue
		}
		taskPath := filepath.Join(dir, taskDir.Name())
		d.pruneTaskDir(taskPath, kind)
	}
}
daemon.Daemon.pruneTaskDir method · go · L1774-L1828 (55 LOC)
internal/daemon/daemon.go
func (d *Daemon) pruneTaskDir(taskPath, kind string) {
	entries, err := os.ReadDir(taskPath)
	if err != nil {
		return
	}

	// Collect files with their modification times
	type fileInfo struct {
		path    string
		modTime time.Time
	}
	var files []fileInfo

	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			continue
		}
		files = append(files, fileInfo{entry.Name(), info.ModTime()})
	}

	// Sort by modification time (oldest first)
	sort.Slice(files, func(i, j int) bool {
		return files[i].modTime.Before(files[j].modTime)
	})

	var toDelete []string
	cutoff := time.Now().Add(-d.config.Retention.MaxAge)

	// Delete by age
	if d.config.Retention.MaxAge > 0 {
		for _, f := range files {
			if f.modTime.Before(cutoff) {
				toDelete = append(toDelete, f.path)
			}
		}
	}

	// Delete by count (keep MaxRuns most recent)
	if d.config.Retention.MaxRuns > 0 && len(files) > d.config.Retention.MaxRuns {
		keep := len(files) - d.config.Retention.MaxRuns
		// files are
daemon.loadWatchedPaths function · go · L1830-L1873 (44 LOC)
internal/daemon/daemon.go
func loadWatchedPaths() []string {
	watchedDir := config.WatchedDir()
	dirs, err := os.ReadDir(watchedDir)
	if err != nil {
		return nil
	}

	var paths []string
	for _, d := range dirs {
		if !d.IsDir() {
			continue
		}

		dirPath := filepath.Join(watchedDir, d.Name())
		entries, err := os.ReadDir(dirPath)
		if err != nil {
			continue
		}

		// Sort descending to get latest file first
		sort.Slice(entries, func(i, j int) bool {
			return entries[i].Name() > entries[j].Name()
		})

		for _, e := range entries {
			if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
				continue
			}

			data, err := os.ReadFile(filepath.Join(dirPath, e.Name()))
			if err != nil {
				break
			}

			p := parseWatchedPath(string(data))
			if p != "" {
				paths = append(paths, p)
			}
			break
		}
	}

	return paths
}
daemon.parseWatchedPath function · go · L1879-L1894 (16 LOC)
internal/daemon/daemon.go
func parseWatchedPath(content string) string {
	start := strings.Index(content, "---\n")
	if start == -1 {
		return ""
	}
	end := strings.Index(content[start+4:], "\n---")
	if end == -1 {
		return ""
	}

	var fm watchFrontmatter
	if err := yaml.Unmarshal([]byte(content[start+4:start+4+end]), &fm); err != nil {
		return ""
	}
	return fm.Path
}
Want this analysis on your repo? https://repobility.com/scan/
daemon.IsDaemonRunning function · go · L1897-L1904 (8 LOC)
internal/daemon/daemon.go
func IsDaemonRunning() bool {
	conn, err := net.Dial("unix", filepath.Join(config.Dir(), "daemon.sock"))
	if err != nil {
		return false
	}
	conn.Close()
	return true
}
daemon.socketClient function · go · L1907-L1916 (10 LOC)
internal/daemon/daemon.go
func socketClient() *http.Client {
	sockPath := filepath.Join(config.Dir(), "daemon.sock")
	return &http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return net.Dial("unix", sockPath)
			},
		},
	}
}
daemon.SendPsRequest function · go · L1919-L1955 (37 LOC)
internal/daemon/daemon.go
func SendPsRequest() ([]TaskInfo, error) {
	resp, err := socketClient().Get("http://daemon/ps")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var tasks []TaskInfo
	if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
		return nil, err
	}

	return tasks, nil
}

// SendStatusRequest queries the daemon's /status endpoint.
func SendStatusRequest() (*DaemonStatus, error) {
	resp, err := socketClient().Get("http://daemon/status")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var status DaemonStatus
	if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
		return nil, err
	}
	return &status, nil
}
daemon.SendStatusRequest function · go · L1939-L1975 (37 LOC)
internal/daemon/daemon.go
func SendStatusRequest() (*DaemonStatus, error) {
	resp, err := socketClient().Get("http://daemon/status")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var status DaemonStatus
	if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
		return nil, err
	}
	return &status, nil
}

// SendTimeoutRequest queries the daemon's /timeout endpoint and returns task timeout info.
func SendTimeoutRequest() ([]TaskInfo, error) {
	resp, err := socketClient().Get("http://daemon/timeout")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var tasks []TaskInfo
	if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
		return nil, err
	}

	return tasks, nil
}
daemon.SendTimeoutRequest function · go · L1958-L1995 (38 LOC)
internal/daemon/daemon.go
func SendTimeoutRequest() ([]TaskInfo, error) {
	resp, err := socketClient().Get("http://daemon/timeout")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var tasks []TaskInfo
	if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
		return nil, err
	}

	return tasks, nil
}

// SendQueueRequest queries the daemon's /queue endpoint and returns queue status.
func SendQueueRequest() ([]TaskQueueInfo, error) {
	resp, err := socketClient().Get("http://daemon/queue")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var tasks []TaskQueueInfo
	if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
		return nil, err
	}

	return tasks, nil
}
daemon.SendQueueRequest function · go · L1978-L2010 (33 LOC)
internal/daemon/daemon.go
func SendQueueRequest() ([]TaskQueueInfo, error) {
	resp, err := socketClient().Get("http://daemon/queue")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("daemon request failed: %s", resp.Status)
	}

	var tasks []TaskQueueInfo
	if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
		return nil, err
	}

	return tasks, nil
}

// SendDrainRequest tells the daemon to stop-on-idle (daemon-wide).
func SendDrainRequest() error {
	resp, err := socketClient().Post("http://daemon/drain", "application/json", bytes.NewBufferString("{}"))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon drain failed: %s", string(body))
	}
	return nil
}
daemon.SendDrainRequest function · go · L1998-L2030 (33 LOC)
internal/daemon/daemon.go
func SendDrainRequest() error {
	resp, err := socketClient().Post("http://daemon/drain", "application/json", bytes.NewBufferString("{}"))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon drain failed: %s", string(body))
	}
	return nil
}

// SendDrainTaskRequest marks a specific task (by ID) for stop-on-idle.
func SendDrainTaskRequest(id string) error {
	data, err := json.Marshal(DrainTaskRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/drain/task", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon drain task failed: %s", string(body))
	}
	return nil
}
daemon.SendDrainTaskRequest function · go · L2013-L2051 (39 LOC)
internal/daemon/daemon.go
func SendDrainTaskRequest(id string) error {
	data, err := json.Marshal(DrainTaskRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/drain/task", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon drain task failed: %s", string(body))
	}
	return nil
}

// SendRunRequest asks the daemon to immediately dispatch a task.
func SendRunRequest(projectPath, taskID, taskName string) error {
	data, err := json.Marshal(RunRequest{ProjectPath: projectPath, TaskID: taskID, TaskName: taskName})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/run", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("%s", strings.TrimSpace(stri
Open data scored by Repobility · https://repobility.com
daemon.SendRunRequest function · go · L2033-L2071 (39 LOC)
internal/daemon/daemon.go
func SendRunRequest(projectPath, taskID, taskName string) error {
	data, err := json.Marshal(RunRequest{ProjectPath: projectPath, TaskID: taskID, TaskName: taskName})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/run", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("%s", strings.TrimSpace(string(body)))
	}

	return nil
}

// SendStopRequest tells the daemon to permanently stop a persistent task.
func SendStopRequest(id string) error {
	data, err := json.Marshal(StopRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/stop", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon stop failed: %s", string(body))
	
daemon.SendStopRequest function · go · L2054-L2091 (38 LOC)
internal/daemon/daemon.go
func SendStopRequest(id string) error {
	data, err := json.Marshal(StopRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/stop", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon stop failed: %s", string(body))
	}
	return nil
}

// SendStartRequest tells the daemon to start a stopped persistent task.
func SendStartRequest(projectPath, taskID, taskName string) error {
	data, err := json.Marshal(StartRequest{ProjectPath: projectPath, TaskID: taskID, TaskName: taskName})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/start", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon start failed: %s", string(bod
daemon.SendStartRequest function · go · L2074-L2112 (39 LOC)
internal/daemon/daemon.go
func SendStartRequest(projectPath, taskID, taskName string) error {
	data, err := json.Marshal(StartRequest{ProjectPath: projectPath, TaskID: taskID, TaskName: taskName})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/start", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon start failed: %s", string(body))
	}
	return nil
}

// SendKillRequest sends a kill request to the daemon
func SendKillRequest(id string) error {
	data, err := json.Marshal(KillRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/kill", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon kill failed: %s", string(body))
	}

	return nil
daemon.SendKillRequest function · go · L2094-L2128 (35 LOC)
internal/daemon/daemon.go
func SendKillRequest(id string) error {
	data, err := json.Marshal(KillRequest{ID: id})
	if err != nil {
		return err
	}

	resp, err := socketClient().Post("http://daemon/kill", "application/json", bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon kill failed: %s", string(body))
	}

	return nil
}

// SendReloadRequest sends a reload request to the daemon to reload its config.
func SendReloadRequest() error {
	resp, err := socketClient().Post("http://daemon/reload", "application/json", bytes.NewBufferString("{}"))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("daemon reload failed: %s", string(body))
	}

	return nil
}
daemon.newDaemonLogger function · go · L50-L56 (7 LOC)
internal/daemon/logger.go
func newDaemonLogger() *daemonLogger {
	fi, err := os.Stdout.Stat()
	tty := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
	l := &daemonLogger{isTTY: tty}
	l.openLogFile()
	return l
}
daemon.daemonLogger.openLogFile method · go · L61-L88 (28 LOC)
internal/daemon/logger.go
func (l *daemonLogger) openLogFile() {
	if err := config.EnsureDir(); err != nil {
		return
	}
	path := config.DaemonLogPath()

	// When daemonized, stdout is redirected to daemon.log by the parent process.
	// Detect this to avoid writing each line twice.
	if stdoutInfo, err := os.Stdout.Stat(); err == nil {
		if logInfo, err := os.Stat(path); err == nil {
			if os.SameFile(stdoutInfo, logInfo) {
				return
			}
		}
	}

	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return
	}
	info, err := f.Stat()
	if err != nil {
		f.Close()
		return
	}
	l.logFile = f
	l.logSize = info.Size()
}
daemon.daemonLogger.rotateIfNeeded method · go · L92-L102 (11 LOC)
internal/daemon/logger.go
func (l *daemonLogger) rotateIfNeeded() {
	if l.logFile == nil || l.logSize < maxLogSize {
		return
	}
	l.logFile.Close()
	path := config.DaemonLogPath()
	_ = os.Rename(path, path+".1")
	l.logFile = nil
	l.logSize = 0
	l.openLogFile()
}
daemon.daemonLogger.c method · go · L105-L110 (6 LOC)
internal/daemon/logger.go
func (l *daemonLogger) c(code, text string) string {
	if !l.isTTY || code == "" {
		return text
	}
	return code + text + ansiReset
}
Source: Repobility analyzer · https://repobility.com
daemon.daemonLogger.priorityStr method · go · L112-L119 (8 LOC)
internal/daemon/logger.go
func (l *daemonLogger) priorityStr(p int) string {
	s := fmt.Sprintf("p%d", p)
	idx := p
	if idx >= len(priorityColors) {
		idx = len(priorityColors) - 1
	}
	return l.c(priorityColors[idx], s)
}
daemon.daemonLogger.println method · go · L134-L146 (13 LOC)
internal/daemon/logger.go
func (l *daemonLogger) println(line string) {
	fmt.Fprintln(os.Stdout, line)

	// Write a plain (no ANSI) copy to the log file.
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.logFile != nil {
		plain := ansiPattern.ReplaceAllString(line, "") + "\n"
		n, _ := l.logFile.WriteString(plain)
		l.logSize += int64(n)
		l.rotateIfNeeded()
	}
}
daemon.daemonLogger.WorkerDone method · go · L187-L192 (6 LOC)
internal/daemon/logger.go
func (l *daemonLogger) WorkerDone(id int, projName, name string, elapsed time.Duration) {
	label := projName + "/" + name
	l.println(fmt.Sprintf("%s %s %s  done %s (%s)",
		l.ts(), l.workerStr(id), l.c(ansiGreen, "✓"), l.c(ansiBold, label),
		l.c(ansiDim, elapsed.Round(time.Second).String())))
}
diagnostic.All function · go · L26-L36 (11 LOC)
internal/diagnostic/diagnostics.go
func All() []CheckResult {
	var results []CheckResult

	results = append(results, checkDaemon()...)
	results = append(results, checkConfig()...)
	results = append(results, checkWatchedProjects()...)
	results = append(results, checkTasks()...)
	results = append(results, checkRecentRuns()...)

	return results
}
diagnostic.checkDaemon function · go · L39-L103 (65 LOC)
internal/diagnostic/diagnostics.go
func checkDaemon() []CheckResult {
	var results []CheckResult

	// Check PID file
	pidPath := config.PidFile()
	pidData, err := os.ReadFile(pidPath)
	if os.IsNotExist(err) {
		results = append(results, CheckResult{
			Name:   "daemon_pid",
			Status: "warn",
			Message: "PID file does not exist",
			Fix:    "Start the daemon with 'anvil watch'",
		})
		// Can't check further without PID
		return results
	}

	var pid int
	if _, err := fmt.Sscanf(strings.TrimSpace(string(pidData)), "%d", &pid); err != nil {
		results = append(results, CheckResult{
			Name:   "daemon_pid",
			Status: "fail",
			Message: "PID file is malformed",
			Fix:    "Remove " + pidPath + " and restart daemon",
		})
		return results
	}

	// Check if process is running
	if err := syscallExists(pid); err != nil {
		results = append(results, CheckResult{
			Name:   "daemon_process",
			Status: "fail",
			Message: fmt.Sprintf("Daemon process (PID %d) is not running", pid),
			Fix:    "Remove " + pidPath + " and restart d
diagnostic.checkConfig function · go · L106-L179 (74 LOC)
internal/diagnostic/diagnostics.go
func checkConfig() []CheckResult {
	var results []CheckResult

	cfgPath := config.Path()

	// Check if config exists
	if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
		results = append(results, CheckResult{
			Name:   "config_exists",
			Status: "warn",
			Message: "Config file does not exist",
			Fix:    "Run 'anvil init' to create a default config",
		})
		return results
	}

	results = append(results, CheckResult{
		Name:   "config_exists",
		Status: "pass",
		Message: "Config file exists",
	})

	// Try to parse config
	cfg, err := config.Load()
	if err != nil {
		results = append(results, CheckResult{
			Name:   "config_parse",
			Status: "fail",
			Message: fmt.Sprintf("Config file is invalid: %v", err),
			Fix:    "Fix the syntax errors in " + cfgPath,
		})
		return results
	}

	results = append(results, CheckResult{
		Name:   "config_parse",
		Status: "pass",
		Message: "Config file parses successfully",
	})

	// Check runners are configured
	if len(cfg.Runners) == 0 {
		resu
diagnostic.checkWatchedProjects function · go · L182-L242 (61 LOC)
internal/diagnostic/diagnostics.go
func checkWatchedProjects() []CheckResult {
	var results []CheckResult

	watchedDir := config.WatchedDir()
	entries, err := os.ReadDir(watchedDir)
	if os.IsNotExist(err) {
		results = append(results, CheckResult{
			Name:   "watched_projects",
			Status: "warn",
			Message: "No watched projects directory",
			Fix:    "Add a project with 'anvil project add <path>'",
		})
		return results
	}

	if len(entries) == 0 {
		results = append(results, CheckResult{
			Name:   "watched_projects",
			Status: "warn",
			Message: "No projects being watched",
			Fix:    "Add a project with 'anvil project add <path>'",
		})
		return results
	}

	var missing []string
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		path := filepath.Join(watchedDir, e.Name())
		target, err := os.Readlink(path)
		if err != nil {
			continue // not a symlink, skip
		}
		// Resolve relative symlinks
		if !filepath.IsAbs(target) {
			target = filepath.Join(filepath.Dir(path), target)
		}
		if _, err := os.Sta
diagnostic.checkTasks function · go · L245-L348 (104 LOC)
internal/diagnostic/diagnostics.go
func checkTasks() []CheckResult {
	var results []CheckResult

	watchedDir := config.WatchedDir()
	entries, err := os.ReadDir(watchedDir)
	if os.IsNotExist(err) || len(entries) == 0 {
		// No projects, nothing to check
		return results
	}

	var staleLocks []string
	var invalidCron []string
	var missingIDs []string

	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		projPath := filepath.Join(watchedDir, e.Name())
		target, err := os.Readlink(projPath)
		if err != nil {
			continue
		}
		if !filepath.IsAbs(target) {
			target = filepath.Join(filepath.Dir(projPath), target)
		}

		proj, err := project.Load(target)
		if err != nil {
			continue
		}

		todos, err := proj.LoadTodos()
		if err != nil {
			continue
		}

		for _, todo := range todos {
			// Check for stale locks
			if todo.IsLocked {
				staleLocks = append(staleLocks, todo.Name)
			}

			// Check for missing ID
			if todo.ID == "" {
				missingIDs = append(missingIDs, todo.Name)
			}

			// Check for invalid cron
Repobility · code-quality intelligence · https://repobility.com
diagnostic.checkRecentRuns function · go · L351-L444 (94 LOC)
internal/diagnostic/diagnostics.go
func checkRecentRuns() []CheckResult {
	var results []CheckResult

	watchedDir := config.WatchedDir()
	entries, err := os.ReadDir(watchedDir)
	if os.IsNotExist(err) || len(entries) == 0 {
		return results
	}

	type failureInfo struct {
		taskID  string
		name    string
		count   int
	}
	var failures []failureInfo

	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		projPath := filepath.Join(watchedDir, e.Name())
		target, err := os.Readlink(projPath)
		if err != nil {
			continue
		}
		if !filepath.IsAbs(target) {
			target = filepath.Join(filepath.Dir(projPath), target)
		}

		proj, err := project.Load(target)
		if err != nil {
			continue
		}

		todos, err := proj.LoadTodos()
		if err != nil {
			continue
		}

		for _, todo := range todos {
			if todo.ID == "" {
				continue
			}

			// Check last 10 runs
			records, err := project.ReadAllRunRecords(target, todo.ID)
			if err != nil {
				continue
			}

			// Only check runs from the last 24 hours
			cutoff := time.Now()
diagnostic.syscallExists function · go · L447-L458 (12 LOC)
internal/diagnostic/diagnostics.go
func syscallExists(pid int) error {
	// On Unix, we can use kill(pid, 0) to check if process exists
	process, err := os.FindProcess(pid)
	if err != nil {
		return err
	}
	err = process.Signal(os.Signal(nil))
	if err != nil {
		return err
	}
	return nil
}
project.LoadConfig function · go · L52-L65 (14 LOC)
internal/project/project.go
func LoadConfig(projectPath string) (Config, error) {
	data, err := os.ReadFile(ConfigPath(projectPath))
	if err != nil {
		if os.IsNotExist(err) {
			return Config{}, nil
		}
		return Config{}, fmt.Errorf("reading project config: %w", err)
	}
	var cfg Config
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return Config{}, fmt.Errorf("parsing project config: %w", err)
	}
	return cfg, nil
}
project.Load function · go · L120-L126 (7 LOC)
internal/project/project.go
func Load(path string) (*Project, error) {
	cfg, err := LoadConfig(path)
	if err != nil {
		return nil, err
	}
	return &Project{Path: path, Config: cfg}, nil
}
project.Project.LoadTodos method · go · L131-L295 (165 LOC)
internal/project/project.go
func (p *Project) LoadTodos() ([]Todo, error) {
	todosDir := filepath.Join(p.Path, ".anvil", "todos")
	defaults := p.Config.Defaults
	var todos []Todo

	for pri := 0; pri <= 9; pri++ {
		dir := filepath.Join(todosDir, fmt.Sprintf("p%d", pri))
		entries, err := os.ReadDir(dir)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, fmt.Errorf("reading todos p%d: %w", pri, err)
		}

		// Sort by name so oldest-timestamped files come first
		sort.Slice(entries, func(i, j int) bool {
			return entries[i].Name() < entries[j].Name()
		})

		for _, e := range entries {
			if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
				continue
			}
			fp := filepath.Join(dir, e.Name())
			raw, err := os.ReadFile(fp)
			if err != nil {
				continue
			}

			// Check for lock file (stale lock indicates daemon crashed mid-execution)
			lockPath := fp + ".lock"
			_, lockErr := os.Stat(lockPath)
			hasLock := lockErr == nil

			// Parse optional front-matter for a schedule.
			// 
project.applyDefaults function · go · L300-L369 (70 LOC)
internal/project/project.go
func applyDefaults(defaults TaskDefaults, fmKeys map[string]interface{},
	skipPermissions *bool, allowedTools *[]string, preCheck *string,
	onSuccess *string, onFailure *string, timeout *time.Duration,
	retry *int, retryDelay *time.Duration, maxConcurrent *int,
	persistentCooldown *time.Duration, persistentMaxRuntime *time.Duration,
	persistentBudget *time.Duration, maxLogSize *int64, runnerOverride *string) {

	has := func(key string) bool {
		if fmKeys == nil {
			return false
		}
		_, ok := fmKeys[key]
		return ok
	}

	if !has("skip_permissions") && defaults.SkipPermissions {
		*skipPermissions = defaults.SkipPermissions
	}
	if !has("allowed_tools") && len(defaults.AllowedTools) > 0 {
		*allowedTools = defaults.AllowedTools
	}
	if !has("pre_check") && defaults.PreCheck != "" {
		*preCheck = defaults.PreCheck
	}
	if !has("on_success") && defaults.OnSuccess != "" {
		*onSuccess = defaults.OnSuccess
	}
	if !has("on_failure") && defaults.OnFailure != "" {
		*onFailure = defaults.OnFailu
project.RemoveLock function · go · L383-L394 (12 LOC)
internal/project/project.go
func RemoveLock(todo Todo) error {
	lockPath := todo.Path + ".lock"
	_, err := os.Stat(lockPath)
	if os.IsNotExist(err) {
		// No lock file to remove
		return nil
	}
	if err != nil {
		return fmt.Errorf("checking lock file %s: %w", lockPath, err)
	}
	return os.Remove(lockPath)
}
project.Init function · go · L399-L411 (13 LOC)
internal/project/project.go
func Init(path string, toolsFS fs.FS) error {
	todosDir := filepath.Join(path, ".anvil", "todos")
	if err := os.MkdirAll(todosDir, 0755); err != nil {
		return fmt.Errorf("creating .anvil/todos: %w", err)
	}

	claudeDir := filepath.Join(path, ".claude")
	if err := writeEmbeddedFS(claudeDir, toolsFS); err != nil {
		return fmt.Errorf("writing .claude/ tools: %w", err)
	}

	return nil
}
Want this analysis on your repo? https://repobility.com/scan/
project.writeEmbeddedFS function · go · L415-L438 (24 LOC)
internal/project/project.go
func writeEmbeddedFS(destDir string, fsys fs.FS) error {
	return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		target := filepath.Join(destDir, path)

		if d.IsDir() {
			return os.MkdirAll(target, 0755)
		}

		data, err := fs.ReadFile(fsys, path)
		if err != nil {
			return fmt.Errorf("reading embedded %s: %w", path, err)
		}

		if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
			return err
		}

		return os.WriteFile(target, data, 0644)
	})
}
project.Project.AddTodo method · go · L442-L514 (73 LOC)
internal/project/project.go
func (p *Project) AddTodo(priority int, schedule string, content string, preCheck string, allowedTools string, maxConcurrent int, skipPermissions bool, runnerCmd string) (string, error) {
	if priority < 0 || priority > 9 {
		return "", fmt.Errorf("priority must be 0-9, got %d", priority)
	}
	if strings.TrimSpace(content) == "" {
		return "", fmt.Errorf("task content must not be empty")
	}

	// Validate cron expression before writing the task file.
	// Skip validation for "persistent" since it's a special keyword, not a cron expression.
	if schedule != "" && schedule != "persistent" {
		if _, err := cron.Parse(schedule); err != nil {
			return "", fmt.Errorf("invalid schedule %q: %w", schedule, err)
		}
	}

	dir := filepath.Join(p.Path, ".anvil", "todos", fmt.Sprintf("p%d", priority))
	if err := os.MkdirAll(dir, 0755); err != nil {
		return "", fmt.Errorf("creating todos/p%d: %w", priority, err)
	}

	base := slugify(content)
	filename := base + ".md"
	// Avoid silent overwrites on slug 
project.newUUID function · go · L517-L523 (7 LOC)
internal/project/project.go
func newUUID() string {
	var b [16]byte
	_, _ = rand.Read(b[:])
	b[6] = (b[6] & 0x0f) | 0x40 // version 4
	b[8] = (b[8] & 0x3f) | 0x80 // variant 1
	return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
project.SessionPathBySessionID function · go · L534-L539 (6 LOC)
internal/project/project.go
func SessionPathBySessionID(projectPath string, sessionID string) string {
	home, _ := os.UserHomeDir()
	slug := strings.ReplaceAll(projectPath, "/", "-")
	slug = strings.ReplaceAll(slug, "_", "-")
	return filepath.Join(home, ".claude", "projects", slug, sessionID+".jsonl")
}
project.WriteRunRecord function · go · L557-L576 (20 LOC)
internal/project/project.go
func WriteRunRecord(projectPath string, rec RunRecord) error {
	dir := runsDir(projectPath, rec.TaskID)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("creating runs dir: %w", err)
	}

	data, err := json.Marshal(rec)
	if err != nil {
		return fmt.Errorf("marshaling run record: %w", err)
	}

	recPath := filepath.Join(dir, rec.RunID+".json")
	if err := os.WriteFile(recPath, data, 0644); err != nil {
		return fmt.Errorf("writing run record: %w", err)
	}

	// Update "current" pointer to the latest runID
	currentPath := filepath.Join(dir, "current")
	return os.WriteFile(currentPath, []byte(rec.RunID), 0644)
}
project.ReadCurrentRunRecord function · go · L579-L598 (20 LOC)
internal/project/project.go
func ReadCurrentRunRecord(projectPath, taskID string) (RunRecord, error) {
	currentPath := CurrentRunPath(projectPath, taskID)
	runIDBytes, err := os.ReadFile(currentPath)
	if err != nil {
		return RunRecord{}, fmt.Errorf("reading current run pointer: %w", err)
	}

	runID := strings.TrimSpace(string(runIDBytes))
	recPath := RunPath(projectPath, taskID, runID)
	data, err := os.ReadFile(recPath)
	if err != nil {
		return RunRecord{}, fmt.Errorf("reading run record: %w", err)
	}

	var rec RunRecord
	if err := json.Unmarshal(data, &rec); err != nil {
		return RunRecord{}, fmt.Errorf("parsing run record: %w", err)
	}
	return rec, nil
}
project.ReadAllRunRecords function · go · L601-L634 (34 LOC)
internal/project/project.go
func ReadAllRunRecords(projectPath, taskID string) ([]RunRecord, error) {
	dir := runsDir(projectPath, taskID)
	entries, err := os.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("reading runs dir: %w", err)
	}

	var records []RunRecord
	for _, e := range entries {
		if e.IsDir() || e.Name() == "current" || !strings.HasSuffix(e.Name(), ".json") {
			continue
		}
		recPath := filepath.Join(dir, e.Name())
		data, err := os.ReadFile(recPath)
		if err != nil {
			continue // skip unreadable records
		}
		var rec RunRecord
		if err := json.Unmarshal(data, &rec); err != nil {
			continue // skip malformed records
		}
		records = append(records, rec)
	}

	// Sort by start time, newest first
	sort.Slice(records, func(i, j int) bool {
		return records[i].Started.After(records[j].Started)
	})

	return records, nil
}
project.LatestSessionID function · go · L638-L644 (7 LOC)
internal/project/project.go
func LatestSessionID(projectPath, taskID string) (string, error) {
	rec, err := ReadCurrentRunRecord(projectPath, taskID)
	if err == nil {
		return rec.SessionID, nil
	}
	return "", fmt.Errorf("no run record found for task %s", taskID)
}
Open data scored by Repobility · https://repobility.com
project.slugify function · go · L652-L665 (14 LOC)
internal/project/project.go
func slugify(s string) string {
	s = strings.ToLower(s)
	s = reNonAlphaNum.ReplaceAllString(s, "-")
	s = reMultipleHyphen.ReplaceAllString(s, "-")
	s = strings.Trim(s, "-")
	if len(s) > 50 {
		s = s[:50]
		s = strings.TrimRight(s, "-")
	}
	if s == "" {
		s = "task"
	}
	return s
}
project.PruneProject function · go · L28-L49 (22 LOC)
internal/project/retention.go
func PruneProject(projectPath string, opts PruneOptions) PruneResult {
	if opts.Now.IsZero() {
		opts.Now = time.Now()
	}

	var result PruneResult

	// Find all task IDs by scanning the runs directory
	logsBase := filepath.Join(projectPath, ".anvil", "logs")
	runsBase := filepath.Join(projectPath, ".anvil", "runs")

	taskIDs := discoverTaskIDs(logsBase, runsBase)

	for _, taskID := range taskIDs {
		r := pruneTask(logsBase, runsBase, taskID, opts)
		result.LogsDeleted += r.LogsDeleted
		result.RunsDeleted += r.RunsDeleted
		result.Errors = append(result.Errors, r.Errors...)
	}

	return result
}
project.discoverTaskIDs function · go · L52-L73 (22 LOC)
internal/project/retention.go
func discoverTaskIDs(logsBase, runsBase string) []string {
	seen := make(map[string]bool)

	for _, base := range []string{logsBase, runsBase} {
		entries, err := os.ReadDir(base)
		if err != nil {
			continue
		}
		for _, e := range entries {
			if e.IsDir() {
				seen[e.Name()] = true
			}
		}
	}

	ids := make([]string, 0, len(seen))
	for id := range seen {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	return ids
}
‹ prevpage 3 / 4next ›