Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
continue-on-error: true # tech debt tracked in CLAUDE.md (nhooyr.io/websocket migration)
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -73,7 +72,7 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.5.0
version: v2.12.2

build:
name: Build
Expand Down
28 changes: 27 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,36 @@ run:

linters:
default: standard
settings:
errcheck:
# Functions whose errors are uninteresting in production.
# All site-specific dangerous ignores must use //nolint:errcheck
# with a one-line rationale — never blanket-suppressed here.
exclude-functions:
# Writes to stdout/stderr/tabwriter — failures are unrecoverable
# and not actionable (the user already lost the message).
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
- (*text/tabwriter.Writer).Flush
# Best-effort cleanup. Real failures show up via other paths
# (stale lock detection, retried operations).
- (io.Closer).Close
- (*os.File).Close
- (*database/sql.DB).Close
- (*database/sql.Rows).Close
- (*database/sql.Stmt).Close
- (net/http.ResponseWriter).Write
# HTTP response body close: real errors surface as connection
# reset on next request.
- (*net/http.Response).Body.Close
exclusions:
rules:
# Test bodies routinely ignore Close/Append on temp stores —
# failures show up as actual test assertions anyway.
# failures show up as actual test assertions anyway. We also
# skip staticcheck QF (quick-fix) refactor hints in tests; we
# prioritise test readability over micro-optimisations.
- path: _test\.go
linters:
- errcheck
- staticcheck
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,10 @@ Closing summary (24 PRs merged across the bulletproofing pass):
50. ~~YAML criterion path containment + sql_query_returns read-only gate (sqlsafety package split out of cli)~~ — DONE (PR #68).
51. ~~Prompt file 0o644 → 0o600 (registry.go + tmux_runner.go)~~ — DONE (this commit). Prompt content carrying DSNs / WAVE_CONTEXT / acceptance criteria no longer readable by non-owner users on a shared dispatch host.
52. **YAML pipe/semicolon caveat** — DOCUMENTED. `ValidateConfigShellCommand` blocks command substitution but deliberately allows `|`, `;`, `&&` for legitimate multi-step QA commands. An operator who copy-pastes a malicious vxd.yaml can still chain `; curl evil` — this is a documented operator trust boundary, not an oversight. The blocklist is one of three layers; the others are: (a) commands run only when the operator explicitly invokes a requirement that triggers QA, (b) the dashboard auth gate prevents remote requirement submission.
53. ~~Errcheck cleanup + lint job blocking~~ — DONE. `golangci-lint` now reports **0 issues** across the project (`-default standard`, ~5 minute timeout). 44 silent event-store / projection-store `Append`/`Project` failures across `internal/cli` + `internal/engine` now log with full story-ID context; 15 dangerous `f.Write`/`db.Exec`/artifact-store sites now return wrapped errors; best-effort cleanup sites carry explicit `_ =` discards with one-line rationale; `.golangci.yml` excludes benign noise (`fmt.Fprint*` to stdout, `(io.Closer).Close`, HTTP body close, tabwriter Flush) and widens the test-file exemption to cover all linters. The `lint` job in `.github/workflows/ci.yml` lost its `continue-on-error: true` — it is now a blocking gate.

### Still open (tracked, not security-blocking)

- Production errcheck cleanup (~51 hits) — gate for flipping lint job to blocking.
- Coverage roadmap: cli (65.6%), config (70.9%), improve (73%), state (78.2%) → 80%+.
- `sanitize.DetectPromptInjection` pattern expansion — structural `<untrusted_content>` wrapping is the durable defence and is already applied where it matters.
29. **Ephemeral DBs for agents** — COMPLETE as of 2026-05-22. SHIPPED:
Expand Down
18 changes: 12 additions & 6 deletions cmd/vxd-improve/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func main() {
// Skip implementation for non-actionable findings (competitor intel, news)
if !sf.Actionable {
log.Printf(" [proposed] %s (intelligence only, not actionable)", sf.Title)
auditLog.Append(improve.AuditEntry{
if err := auditLog.Append(improve.AuditEntry{
RunID: runID,
FindingID: fmt.Sprintf("f-%s-%03d", date, i+1),
Source: sf.SourceURL,
Expand All @@ -111,7 +111,9 @@ func main() {
Risk: sf.Risk,
Disposition: "proposed",
Reasoning: sf.Reasoning,
})
}); err != nil {
log.Printf(" audit log append (proposed): %v", err)
}
summary.PRsProposed++
continue
}
Expand All @@ -135,7 +137,7 @@ func main() {
summary.Errors = append(summary.Errors, fmt.Sprintf("[%s] %s: %s", result.Disposition, sf.Title, result.Error))
}

auditLog.Append(improve.AuditEntry{
if err := auditLog.Append(improve.AuditEntry{
RunID: runID,
FindingID: fmt.Sprintf("f-%s-%03d", date, i+1),
Source: sf.SourceURL,
Expand All @@ -153,7 +155,9 @@ func main() {
Reasoning: sf.Reasoning,
SecurityReview: af.SecurityReview,
LicenseCheck: af.LicenseCheck,
})
}); err != nil {
log.Printf(" audit log append (%s): %v", result.Disposition, err)
}

log.Printf(" [%s] %s", result.Disposition, sf.Title)
}
Expand Down Expand Up @@ -252,12 +256,14 @@ func main() {

// Update pipeline with proposal data
for _, opp := range proposalResults {
improve.UpdateOpportunityField(pipelinePath, opp.ID, func(existing improve.Opportunity) improve.Opportunity {
if _, err := improve.UpdateOpportunityField(pipelinePath, opp.ID, func(existing improve.Opportunity) improve.Opportunity {
existing.ProposalDraft = opp.ProposalDraft
existing.ProposalDraftedAt = opp.ProposalDraftedAt
existing.Status = improve.StatusProposalDrafted
return existing
})
}); err != nil {
log.Printf(" update opportunity %s: %v", opp.ID, err)
}
}
} else if !cfg.ActiveBidding {
log.Println(" Observation mode — no proposals drafted (set VXD_ACTIVE_BIDDING=true to enable)")
Expand Down
8 changes: 6 additions & 2 deletions internal/cli/approve.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ func runApprovePlan(cmd *cobra.Command, args []string) error {
evt := state.NewEvent(state.EventPlanApproved, "human", "", map[string]any{
"req_id": reqID,
})
s.Events.Append(evt)
s.Proj.Project(evt)
if err := s.Events.Append(evt); err != nil {
return fmt.Errorf("append plan-approved event: %w", err)
}
if err := s.Proj.Project(evt); err != nil {
return fmt.Errorf("project plan-approved event: %w", err)
}

fmt.Fprintf(cmd.OutOrStdout(), "Plan approved for %s. Run 'vxd resume %s' to start dispatch.\n", reqID, reqID)
return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ Audit the connection's privileges accordingly.`,
if err != nil {
return fmt.Errorf("connect: %w", err)
}
defer conn.Close(ctx)
defer func() { _ = conn.Close(ctx) }() // best-effort cleanup

// Mutating + --write path: Exec is the correct verb for
// INSERT/UPDATE/DELETE/DDL because it returns rows-affected
Expand Down
10 changes: 6 additions & 4 deletions internal/cli/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func loadStores(cmd *cobra.Command) (stores, error) {

ps, err := state.NewSQLiteStore(filepath.Join(projectDir, "vxd.db"))
if err != nil {
es.Close()
_ = es.Close() // best-effort cleanup; original error is what matters
return stores{}, fmt.Errorf("open projection store: %w", err)
}

Expand All @@ -104,13 +104,15 @@ func loadStores(cmd *cobra.Command) (stores, error) {
}, nil
}

// Close releases both stores.
// Close releases both stores. Errors are intentionally ignored: callers
// invoke this via defer at process-exit boundaries where there is nowhere
// useful to report a close failure.
func (s stores) Close() {
if s.Events != nil {
s.Events.Close()
_ = s.Events.Close()
}
if s.Proj != nil {
s.Proj.Close()
_ = s.Proj.Close()
}
}

Expand Down
8 changes: 6 additions & 2 deletions internal/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,19 @@ func runInit(cmd *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("initialize event store: %w", err)
}
es.Close()
if err := es.Close(); err != nil {
return fmt.Errorf("close event store: %w", err)
}

// Initialize projection store (SQLite)
dbPath := filepath.Join(vxdDir, "vxd.db")
ps, err := state.NewSQLiteStore(dbPath)
if err != nil {
return fmt.Errorf("initialize projection store: %w", err)
}
ps.Close()
if err := ps.Close(); err != nil {
return fmt.Errorf("close projection store: %w", err)
}

fmt.Fprintf(out, "Initialized VXD workspace at %s\n", vxdDir)
fmt.Fprintf(out, " Event store: %s\n", eventsPath)
Expand Down
6 changes: 4 additions & 2 deletions internal/cli/opportunity.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,13 @@ func runOppPropose(cmd *cobra.Command, args []string) error {
fmt.Println(draft)

// Update pipeline
improve.UpdateOpportunityField(pipelinePath(), id, func(opp improve.Opportunity) improve.Opportunity {
if _, err := improve.UpdateOpportunityField(pipelinePath(), id, func(opp improve.Opportunity) improve.Opportunity {
opp.ProposalDraft = draft
opp.Status = improve.StatusProposalDrafted
return opp
})
}); err != nil {
return fmt.Errorf("update opportunity pipeline: %w", err)
}

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func countProjectStories(vxdRoot, projectName string) (total, merged int) {
if err != nil {
return 0, 0
}
defer ps.Close()
defer func() { _ = ps.Close() }() // best-effort cleanup

allStories, err := ps.ListStories(state.StoryFilter{})
if err != nil {
Expand Down
16 changes: 12 additions & 4 deletions internal/cli/reject.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ func runRejectPlan(cmd *cobra.Command, args []string) error {
"req_id": reqID,
"feedback": feedback,
})
s.Events.Append(evt)
s.Proj.Project(evt)
if err := s.Events.Append(evt); err != nil {
return fmt.Errorf("append plan-rejected event: %w", err)
}
if err := s.Proj.Project(evt); err != nil {
return fmt.Errorf("project plan-rejected event: %w", err)
}

fmt.Fprintf(cmd.OutOrStdout(), "Plan rejected for %s.\nFeedback: %s\nRe-run 'vxd req' with updated requirement.\n", reqID, feedback)
return nil
Expand Down Expand Up @@ -72,8 +76,12 @@ func runReject(cmd *cobra.Command, args []string) error {
"story_id": storyID,
"feedback": feedback,
})
s.Events.Append(evt)
s.Proj.Project(evt)
if err := s.Events.Append(evt); err != nil {
return fmt.Errorf("append story-rejected event: %w", err)
}
if err := s.Proj.Project(evt); err != nil {
return fmt.Errorf("project story-rejected event: %w", err)
}

fmt.Fprintf(cmd.OutOrStdout(), "Rejected: %s\nFeedback: %s\nStory reset to draft for retry.\n", story.Title, feedback)
return nil
Expand Down
24 changes: 18 additions & 6 deletions internal/cli/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ func runResume(cmd *cobra.Command, args []string) error {
"req_id": reqID,
"mode": mode,
})
s.Events.Append(modeEvt)
s.Proj.Project(modeEvt)
if err := s.Events.Append(modeEvt); err != nil {
return fmt.Errorf("append review-mode event: %w", err)
}
if err := s.Proj.Project(modeEvt); err != nil {
return fmt.Errorf("project review-mode event: %w", err)
}
}

// Plan approval gate
Expand Down Expand Up @@ -212,15 +216,23 @@ func runResume(cmd *cobra.Command, args []string) error {
evt := state.NewEvent(state.EventStoryReset, "recovery", issue.StoryID, map[string]any{
"reason": issue.Detail,
})
s.Events.Append(evt)
s.Proj.Project(evt)
if err := s.Events.Append(evt); err != nil {
log.Printf("[resume] append story-reset event for %s: %v", issue.StoryID, err)
}
if err := s.Proj.Project(evt); err != nil {
log.Printf("[resume] project story-reset event for %s: %v", issue.StoryID, err)
}
}
}
recoveryEvt := state.NewEvent(state.EventRecoveryCompleted, "system", "", map[string]any{
"issues_found": len(recoveryIssues),
})
s.Events.Append(recoveryEvt)
s.Proj.Project(recoveryEvt)
if err := s.Events.Append(recoveryEvt); err != nil {
log.Printf("[resume] append recovery-completed event: %v", err)
}
if err := s.Proj.Project(recoveryEvt); err != nil {
log.Printf("[resume] project recovery-completed event: %v", err)
}
}

// Recover orphaned devdb instances left behind by previously crashed pipelines.
Expand Down
9 changes: 6 additions & 3 deletions internal/cli/review_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ func runReviewStory(cmd *cobra.Command, args []string) error {
}

func openURL(url string) {
// All branches: best-effort browser launch; if the helper isn't
// installed (xdg-open missing on a headless Linux box) we just
// continue, the URL is already printed for the user to copy.
switch runtime.GOOS {
case "darwin":
exec.Command("open", url).Start()
_ = exec.Command("open", url).Start()
case "linux":
exec.Command("xdg-open", url).Start()
_ = exec.Command("xdg-open", url).Start()
case "windows":
exec.Command("cmd", "/c", "start", url).Start()
_ = exec.Command("cmd", "/c", "start", url).Start()
}
}
10 changes: 5 additions & 5 deletions internal/codegraph/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (ia *ImpactAnalysis) FormatMarkdown() string {
}

var b strings.Builder
b.WriteString(fmt.Sprintf("## Blast Radius Analysis (risk: %.2f/1.0)\n", ia.RiskScore))
fmt.Fprintf(&b, "## Blast Radius Analysis (risk: %.2f/1.0)\n", ia.RiskScore)
b.WriteString(ia.Summary)
b.WriteString("\n\n")

Expand All @@ -73,8 +73,8 @@ func (ia *ImpactAnalysis) FormatMarkdown() string {
limit = 10
}
for i, n := range ia.ReviewPriorities[:limit] {
b.WriteString(fmt.Sprintf("%d. %s (%s:%d-%d) — risk %.2f\n",
i+1, n.Name, shortPath(n.FilePath), n.LineStart, n.LineEnd, n.RiskScore))
fmt.Fprintf(&b, "%d. %s (%s:%d-%d) — risk %.2f\n",
i+1, n.Name, shortPath(n.FilePath), n.LineStart, n.LineEnd, n.RiskScore)
}
b.WriteString("\n")
}
Expand All @@ -86,8 +86,8 @@ func (ia *ImpactAnalysis) FormatMarkdown() string {
limit = 10
}
for _, g := range ia.TestGaps[:limit] {
b.WriteString(fmt.Sprintf("- %s (%s:%d-%d) — no test coverage\n",
g.Name, shortPath(g.FilePath), g.LineStart, g.LineEnd))
fmt.Fprintf(&b, "- %s (%s:%d-%d) — no test coverage\n",
g.Name, shortPath(g.FilePath), g.LineStart, g.LineEnd)
}
b.WriteString("\n")
}
Expand Down
9 changes: 6 additions & 3 deletions internal/codegraph/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,16 @@ func parseStatus(raw string) (*GraphInfo, error) {
continue
}
key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
// Sscanf errors are intentionally ignored: missing/malformed
// numeric fields fall through with the count at 0, which is the
// correct fallback for an incomplete code-graph stats output.
switch key {
case "Nodes":
fmt.Sscanf(val, "%d", &info.NodeCount)
_, _ = fmt.Sscanf(val, "%d", &info.NodeCount)
case "Edges":
fmt.Sscanf(val, "%d", &info.EdgeCount)
_, _ = fmt.Sscanf(val, "%d", &info.EdgeCount)
case "Files":
fmt.Sscanf(val, "%d", &info.FileCount)
_, _ = fmt.Sscanf(val, "%d", &info.FileCount)
case "Languages":
info.Languages = strings.Split(val, ", ")
case "Last updated":
Expand Down
2 changes: 1 addition & 1 deletion internal/devdb/docker/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func DumpSchema(ctx context.Context, pg *PGConn) (string, error) {
out.WriteString("\nTABLE " + key + "\n")
curr = key
}
out.WriteString(fmt.Sprintf(" %s %s (nullable=%s)\n", col, dtype, nullable))
fmt.Fprintf(&out, " %s %s (nullable=%s)\n", col, dtype, nullable)
}
return out.String(), rows.Err()
}
Loading
Loading