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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
tag: add-dcp-config
author: jay-flowers
category: gotcha
created_at: 2026-08-16T18:39:57Z
identity: add-dcp-config-20260816T183957-jay-flowers
tier: draft
---

When adding new code to an existing file like internal/doctor/checks.go, always check if the new code follows the convention pack rules even if the existing code in the same file doesn't. In the add-dcp-config change, the checkDCPConfig() function initially used string concatenation for filesystem paths (projectDir + "/.opencode/commands") because the existing checkConfigDir() in the same file used the same pattern. All 5 review council agents flagged this as a SC-003 MUST violation — new code should use filepath.Join regardless of pre-existing patterns. The ScaffoldDCP() function in agentkit.go correctly used filepath.Join from the start, making the inconsistency within the same change more notable. Lesson: don't copy anti-patterns from existing code; follow the convention pack rules.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
tag: add-dcp-config
author: jay-flowers
category: gotcha
created_at: 2026-08-16T18:40:02Z
identity: add-dcp-config-20260816T184002-jay-flowers
tier: draft
---

When fixing unchecked errors in test setup code (os.MkdirAll, os.WriteFile, os.ReadFile calls without error checking), apply the fix consistently across ALL test files touched by the change, not just the files where the new tests were added. In the add-dcp-config change, iteration 1 review found unchecked errors in agentkit_test.go and checks_test.go (new test code), but those fixes were not applied to pre-existing unchecked errors in init_test.go which was also modified by the change. The iteration 2 Testing reviewer caught this inconsistency and flagged it as HIGH severity. The pattern to follow: if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("setup MkdirAll: %v", err) } — wrap every setup call with t.Fatalf error checking.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
tag: add-dcp-config
author: jay-flowers
category: pattern
created_at: 2026-08-16T18:40:06Z
identity: add-dcp-config-20260816T184006-jay-flowers
tier: draft
---

The doctor package's Run() function signature was extended from Run(store *db.Store, cfg *config.Config) to Run(store *db.Store, cfg *config.Config, projectDir string) to support per-project checks like checkDCPConfig. The key design decision (D9) was to accept an explicit directory parameter rather than calling os.Getwd() inside the check function. This enables test isolation with t.TempDir() — tests pass a temp directory directly instead of needing unsafe os.Chdir() calls. The os.Getwd() call lives in the CLI layer (cmd/replicator/doctor.go) where it's the caller's responsibility. This pattern should be followed for any future per-project doctor checks.
7 changes: 6 additions & 1 deletion cmd/replicator/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ func runDoctor(cfg *config.Config) error {
}
defer store.Close()

results, err := doctor.Run(store, cfg)
projectDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}

results, err := doctor.Run(store, cfg, projectDir)
if err != nil {
return fmt.Errorf("run checks: %w", err)
}
Expand Down
22 changes: 19 additions & 3 deletions cmd/replicator/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ global database (replicator setup) or any external services.`,
return cmd
}

// runInit creates the .uf/replicator/ directory, seeds cells.json, and
// scaffolds the agent kit into .opencode/. Uses styled output: green for
// created, dim for skipped, yellow for overwritten.
// runInit creates the .uf/replicator/ directory, seeds cells.json,
// scaffolds the agent kit into .opencode/, and creates or updates the
// DCP configuration (.opencode/dcp.jsonc) for protect tag support.
// Uses styled output: green for created, dim for skipped, yellow for
// overwritten.
func runInit(targetDir string, force bool) error {
styles := ui.NewStyles(os.Stdout)
replicatorDir := filepath.Join(targetDir, ".uf", "replicator")
Expand Down Expand Up @@ -76,5 +78,19 @@ func runInit(targetDir string, force bool) error {
}
}

// Scaffold DCP config for protect tag support.
dcpResult, err := agentkit.ScaffoldDCP(targetDir)
if err != nil {
return fmt.Errorf("scaffold DCP config: %w", err)
}
switch dcpResult.Action {
case "created":
fmt.Println(styles.Pass.Render(fmt.Sprintf("created .opencode/%s", dcpResult.Path)))
case "skipped":
fmt.Println(styles.Dim.Render(fmt.Sprintf("skipped .opencode/%s (exists)", dcpResult.Path)))
case "overwritten":
fmt.Println(styles.Warn.Render(fmt.Sprintf("overwritten .opencode/%s (added protectTags)", dcpResult.Path)))
}

return nil
}
71 changes: 62 additions & 9 deletions cmd/replicator/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
"strings"
"testing"
)

Expand Down Expand Up @@ -56,23 +57,43 @@ func TestRunInit_FreshDirectory(t *testing.T) {
t.Errorf("expected agent kit file %s to exist: %v", rel, err)
}
}

// Verify DCP config was created with protectTags.
dcpPath := filepath.Join(dir, ".opencode", "dcp.jsonc")
dcpData, err := os.ReadFile(dcpPath)
if err != nil {
t.Fatalf("dcp.jsonc not created: %v", err)
}
if !strings.Contains(string(dcpData), "protectTags") {
t.Error("dcp.jsonc missing protectTags")
}
if !strings.Contains(string(dcpData), "$schema") {
t.Error("dcp.jsonc missing $schema")
}
}

func TestRunInit_AgentKitSkipsExisting(t *testing.T) {
dir := t.TempDir()

// Pre-create a file that init would scaffold.
forgePath := filepath.Join(dir, ".opencode", "commands", "forge.md")
os.MkdirAll(filepath.Dir(forgePath), 0o755)
if err := os.MkdirAll(filepath.Dir(forgePath), 0o755); err != nil {
t.Fatalf("setup MkdirAll: %v", err)
}
original := []byte("# my custom forge\n")
os.WriteFile(forgePath, original, 0o644)
if err := os.WriteFile(forgePath, original, 0o644); err != nil {
t.Fatalf("setup WriteFile: %v", err)
}

if err := runInit(dir, false); err != nil {
t.Fatalf("runInit: %v", err)
}

// Verify the pre-existing file was NOT overwritten.
data, _ := os.ReadFile(forgePath)
data, err := os.ReadFile(forgePath)
if err != nil {
t.Fatalf("read forge.md: %v", err)
}
if string(data) != string(original) {
t.Errorf("forge.md was overwritten: got %q, want %q", string(data), string(original))
}
Expand All @@ -89,16 +110,23 @@ func TestRunInit_ForceOverwrites(t *testing.T) {

// Pre-create a file that init would scaffold.
forgePath := filepath.Join(dir, ".opencode", "commands", "forge.md")
os.MkdirAll(filepath.Dir(forgePath), 0o755)
if err := os.MkdirAll(filepath.Dir(forgePath), 0o755); err != nil {
t.Fatalf("setup MkdirAll: %v", err)
}
original := []byte("# my custom forge\n")
os.WriteFile(forgePath, original, 0o644)
if err := os.WriteFile(forgePath, original, 0o644); err != nil {
t.Fatalf("setup WriteFile: %v", err)
}

if err := runInit(dir, true); err != nil {
t.Fatalf("runInit with force: %v", err)
}

// Verify the pre-existing file WAS overwritten.
data, _ := os.ReadFile(forgePath)
data, err := os.ReadFile(forgePath)
if err != nil {
t.Fatalf("read forge.md: %v", err)
}
if string(data) == string(original) {
t.Error("forge.md was NOT overwritten despite force=true")
}
Expand All @@ -114,15 +142,20 @@ func TestRunInit_AlreadyInitialized(t *testing.T) {

// Write something to cells.json to verify it's not overwritten.
cellsPath := filepath.Join(dir, ".uf", "replicator", "cells.json")
os.WriteFile(cellsPath, []byte(`[{"id":"test"}]`), 0o644)
if err := os.WriteFile(cellsPath, []byte(`[{"id":"test"}]`), 0o644); err != nil {
t.Fatalf("setup WriteFile: %v", err)
}

// Second init — should be idempotent.
if err := runInit(dir, false); err != nil {
t.Fatalf("second runInit: %v", err)
}

// Verify cells.json was NOT overwritten.
data, _ := os.ReadFile(cellsPath)
data, err := os.ReadFile(cellsPath)
if err != nil {
t.Fatalf("read cells.json: %v", err)
}
if string(data) != `[{"id":"test"}]` {
t.Errorf("cells.json was overwritten: got %q", string(data))
}
Expand All @@ -132,12 +165,32 @@ func TestRunInit_AlreadyInitialized(t *testing.T) {
if _, err := os.Stat(forgePath); err != nil {
t.Errorf("agent kit files should exist after second init: %v", err)
}

// Verify DCP config still exists and content was preserved.
dcpPath := filepath.Join(dir, ".opencode", "dcp.jsonc")
dcpBefore, err := os.ReadFile(dcpPath)
if err != nil {
t.Fatalf("read dcp.jsonc before second init: %v", err)
}
// Re-run init and verify DCP content is unchanged.
if err := runInit(dir, false); err != nil {
t.Fatalf("third runInit: %v", err)
}
dcpAfter, err := os.ReadFile(dcpPath)
if err != nil {
t.Fatalf("read dcp.jsonc after third init: %v", err)
}
if string(dcpAfter) != string(dcpBefore) {
t.Error("dcp.jsonc was modified on re-init")
}
}

func TestRunInit_CustomPath(t *testing.T) {
parent := t.TempDir()
target := filepath.Join(parent, "myproject")
os.MkdirAll(target, 0o755)
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatalf("setup MkdirAll: %v", err)
}

if err := runInit(target, false); err != nil {
t.Fatalf("runInit with custom path: %v", err)
Expand Down
63 changes: 63 additions & 0 deletions internal/agentkit/agentkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
)

//go:embed content/*
Expand All @@ -22,6 +23,68 @@ type ScaffoldResult struct {
Action string `json:"action"` // "created", "skipped", "overwritten"
Comment thread
jflowers marked this conversation as resolved.
}

// dcpConfigContent is the canonical DCP config that enables protectTags.
// This matches the replicator repo's own .opencode/dcp.jsonc.
const dcpConfigContent = `{
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json",
// Enable <protect> tag preservation during DCP compression.
// Slash command files in .opencode/commands/ use <protect> tags
// to mark execution-critical sections (guardrails, checklists,
// mandatory gates) that must survive context pruning.
"compress": {
"protectTags": true
}
}
`

// ScaffoldDCP creates or overwrites the DCP configuration file in
// targetDir/.opencode/. It checks for dcp.jsonc first, then dcp.json.
// If neither exists, it creates dcp.jsonc. If one exists with protectTags
// already configured, it is skipped. If one exists without protectTags,
// the file is overwritten with the canonical DCP config content.
func ScaffoldDCP(targetDir string) (ScaffoldResult, error) {
openCodeDir := filepath.Join(targetDir, ".opencode")
jsoncPath := filepath.Join(openCodeDir, "dcp.jsonc")
jsonPath := filepath.Join(openCodeDir, "dcp.json")

// Check .jsonc first, then .json (D3: prefer .jsonc).
var existingPath, fileName string
if _, err := os.Stat(jsoncPath); err == nil {
existingPath = jsoncPath
fileName = "dcp.jsonc"
} else if _, err := os.Stat(jsonPath); err == nil {
existingPath = jsonPath
fileName = "dcp.json"
}

if existingPath != "" {
// File exists — check for protectTags (D2: string scan).
data, err := os.ReadFile(existingPath)
if err != nil {
return ScaffoldResult{}, fmt.Errorf("read %s: %w", fileName, err)
}

if strings.Contains(string(data), "protectTags") {
return ScaffoldResult{Path: fileName, Action: "skipped"}, nil
}

// File exists but lacks protectTags — replace with canonical content (D10).
if err := os.WriteFile(existingPath, []byte(dcpConfigContent), 0o644); err != nil {
return ScaffoldResult{}, fmt.Errorf("write %s: %w", fileName, err)
}
return ScaffoldResult{Path: fileName, Action: "overwritten"}, nil
}

// Neither file exists — create .opencode/dcp.jsonc.
if err := os.MkdirAll(openCodeDir, 0o755); err != nil {
return ScaffoldResult{}, fmt.Errorf("create .opencode directory: %w", err)
}
if err := os.WriteFile(jsoncPath, []byte(dcpConfigContent), 0o644); err != nil {
return ScaffoldResult{}, fmt.Errorf("write dcp.jsonc: %w", err)
}
return ScaffoldResult{Path: "dcp.jsonc", Action: "created"}, nil
}

// Scaffold writes the embedded agent kit files to targetDir/.opencode/.
// If force is false, existing files are skipped. If force is true,
// existing files are overwritten.
Expand Down
Loading
Loading