feat(v0.6): AGENTS.md seed + CLAUDE.md shim + handoff + context-archive scaffold
New workspaces get:
- AGENTS.md (always): canonical agent instructions — handoff workflow,
notes-archive convention (~300-500 line trigger), cross-workspace
discovery, do-not-touch warnings. Project variant adds workspace-
structure and git-conventions sections.
- CLAUDE.md shim (claude type only per v0.6 spec §6 — opencode shim
deferred until its instruction-file convention is verified).
- handoff.md: minimal current-state template the agent updates per
session.
- context/notes-archive/.gitkeep: directory pinned for git tracking,
ready for the agent to populate per the archive convention.
seed.ClaudeMD and seed.ClaudeMDProject removed — no callers remain.
Existing workspaces are NOT modified; this is strictly a ctask-new code
path. The seed-wins overlay rule still applies — a user seed dir's
AGENTS.md/CLAUDE.md overrides the built-in.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package seed
|
||||
|
||||
// AgentsMD returns the canonical AGENTS.md content for a new workspace.
|
||||
// This is the single source of truth for agent-facing instructions in
|
||||
// v0.6+. Agent-specific files (CLAUDE.md, etc.) become thin shims
|
||||
// pointing back here. Per v0.6 spec §6.
|
||||
//
|
||||
// isProject controls a small set of project-specific additions
|
||||
// (workspace-structure section, git conventions).
|
||||
func AgentsMD(isProject bool) string {
|
||||
base := `# Workspace Instructions
|
||||
|
||||
This workspace uses ctask. AGENTS.md is the canonical instruction file
|
||||
for agents working here. Agent-specific files (CLAUDE.md, etc.) are
|
||||
thin shims that defer to this document.
|
||||
|
||||
## Session Workflow
|
||||
|
||||
- Read handoff.md first when starting a session. It carries the
|
||||
current-state briefing: last completed work, immediate next step,
|
||||
blockers, and files to inspect first.
|
||||
- Update notes.md with decisions, design rationale, and observations
|
||||
during the session.
|
||||
- Update handoff.md before ending the session so the next agent (or
|
||||
the next instance of you) can pick up cleanly. Keep it short and
|
||||
current -- handoff.md is not a history file.
|
||||
|
||||
## Notes Archival
|
||||
|
||||
When notes.md becomes too large to scan comfortably (roughly 300-500
|
||||
lines or 10-20 KB), archive older completed-phase sections:
|
||||
|
||||
1. Create a new file: context/notes-archive/YYYY-MM-DD-topic.md
|
||||
2. Move the older sections into it, preserving them exactly.
|
||||
3. Add a pointer at the top of notes.md:
|
||||
"Older notes archived in context/notes-archive/YYYY-MM-DD-topic.md"
|
||||
4. Do not delete or summarize-away historical notes as the only
|
||||
preservation mechanism.
|
||||
5. Keep handoff.md short and current. It is not a history file.
|
||||
|
||||
## Cross-Workspace Context
|
||||
|
||||
Related work may exist in other ctask workspaces. Use:
|
||||
|
||||
ctask list --all discover all workspaces, including archived
|
||||
ctask info <workspace> view metadata and status
|
||||
ctask notes <workspace> read another workspace's notes.md
|
||||
ctask path <workspace> get the filesystem path
|
||||
|
||||
Treat other workspaces as read-only unless the user explicitly asks
|
||||
otherwise.
|
||||
|
||||
## Do Not Touch
|
||||
|
||||
- Do not modify .ctask/ -- those are ctask internals (lease, manifests,
|
||||
session summary, write lock).
|
||||
- Do not edit task.yaml's metadata fields by hand. ctask owns them.
|
||||
`
|
||||
if !isProject {
|
||||
return base + `
|
||||
## File Placement (task workspace)
|
||||
|
||||
- Source code and scripts -> workspace root or src/
|
||||
- Documentation, summaries, reports -> docs/
|
||||
- Deliverables and exports -> output/
|
||||
- Reference material and imported files -> context/
|
||||
- Do not place non-code outputs in the workspace root.
|
||||
`
|
||||
}
|
||||
return base + `
|
||||
## Workspace Structure (project workspace)
|
||||
|
||||
This workspace uses ctask's project layout. The workspace root contains
|
||||
ctask management files; your project code lives in the project
|
||||
subdirectory (named after the workspace slug). When working on project
|
||||
code, operate inside the project subdirectory.
|
||||
|
||||
- Workspace root: ctask metadata, session logs, notes, reference material
|
||||
- Project subdirectory: source code, project-specific configuration
|
||||
- context/: reference material and imported specs (workspace level)
|
||||
- output/: deliverables and exports (workspace level)
|
||||
- logs/: session logs (managed by ctask)
|
||||
|
||||
## Git Conventions
|
||||
|
||||
This workspace uses a single git repository initialized at the workspace
|
||||
root. The project subdirectory and all its contents are tracked by this
|
||||
root repo. Do not initialize additional git repositories inside the
|
||||
project subdirectory or elsewhere -- the repo root is the workspace root.
|
||||
`
|
||||
}
|
||||
|
||||
// ClaudeShimMD returns the thin CLAUDE.md generated for new workspaces
|
||||
// whose agent.type is "claude". It exists purely to point Claude Code at
|
||||
// AGENTS.md; canonical instructions live there. Per v0.6 spec §6.
|
||||
func ClaudeShimMD() string {
|
||||
return `<!-- Generated by ctask. Edit AGENTS.md for canonical instructions.
|
||||
This file may be regenerated by future ctask agent-management commands. -->
|
||||
|
||||
# Claude Code Instructions
|
||||
|
||||
This workspace uses AGENTS.md as the canonical agent guidance file.
|
||||
Read AGENTS.md first.
|
||||
|
||||
## Claude-specific notes
|
||||
|
||||
- Use claude CLI conventions for file operations.
|
||||
- Respect the workspace structure described in AGENTS.md.
|
||||
`
|
||||
}
|
||||
|
||||
// HandoffMD returns the minimal handoff.md template seeded into new
|
||||
// workspaces. The agent fills it in at session end; the next session
|
||||
// reads it first. Per v0.6 spec §8.
|
||||
func HandoffMD() string {
|
||||
return `# Handoff
|
||||
|
||||
## Current state
|
||||
|
||||
New workspace. No work completed yet.
|
||||
|
||||
## Next step
|
||||
|
||||
[Describe the task or project goal here.]
|
||||
|
||||
## Files to read first
|
||||
|
||||
- AGENTS.md - workspace instructions and conventions
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgentsMDContainsHandoffWorkflow(t *testing.T) {
|
||||
body := AgentsMD(false)
|
||||
for _, want := range []string{
|
||||
"handoff.md", // mentions the handoff file
|
||||
"Read handoff.md first", // session-start convention
|
||||
"notes.md", // mentions notes
|
||||
"context/notes-archive/", // archive convention
|
||||
"300", // size hint
|
||||
"ctask list", // cross-workspace discovery
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("AGENTS.md missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsMDProjectVariantHasGitConventions(t *testing.T) {
|
||||
body := AgentsMD(true)
|
||||
for _, want := range []string{
|
||||
"git", // git mentioned for projects
|
||||
"Workspace Structure", // project-specific section
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("AGENTS.md (project) missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeShimMDPointsAtAgentsMD(t *testing.T) {
|
||||
body := ClaudeShimMD()
|
||||
for _, want := range []string{
|
||||
"Generated by ctask",
|
||||
"AGENTS.md",
|
||||
"Read AGENTS.md first",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("CLAUDE.md shim missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandoffMDIsMinimalTemplate(t *testing.T) {
|
||||
body := HandoffMD()
|
||||
for _, want := range []string{
|
||||
"# Handoff",
|
||||
"Current state",
|
||||
"Next step",
|
||||
"AGENTS.md",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("handoff.md missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsMDIsASCII(t *testing.T) {
|
||||
for _, body := range []string{AgentsMD(false), AgentsMD(true), ClaudeShimMD(), HandoffMD()} {
|
||||
for i, b := range []byte(body) {
|
||||
if b > 127 {
|
||||
t.Errorf("non-ASCII byte 0x%02x at position %d", b, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,131 +2,6 @@ package seed
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ClaudeMD returns the built-in default CLAUDE.md content for a task workspace.
|
||||
// Parameters are accepted for API compatibility but are not interpolated in v0.3.
|
||||
func ClaudeMD(slug, category, workspacePath string) string {
|
||||
_ = slug
|
||||
_ = category
|
||||
_ = workspacePath
|
||||
return `# Workspace Guidelines
|
||||
|
||||
This is a ctask workspace. Prefer operating inside this directory unless explicitly instructed otherwise.
|
||||
|
||||
## File Placement
|
||||
|
||||
- Source code and scripts -> workspace root or ` + "`src/`" + `
|
||||
- Documentation, summaries, reports -> ` + "`docs/`" + `
|
||||
- Deliverables and exports -> ` + "`output/`" + `
|
||||
- Reference material and imported files -> ` + "`context/`" + `
|
||||
- Do not place non-code outputs (docs, summaries, exports) in the workspace root
|
||||
|
||||
## Conventions
|
||||
|
||||
- Do not install global packages or modify system files unless asked
|
||||
- Record important assumptions and actions in notes.md
|
||||
- Keep the workspace root clean -- use subdirectories for organization
|
||||
|
||||
## Cross-Workspace Context
|
||||
|
||||
Related work may exist in other ctask workspaces. For project continuation,
|
||||
migration, debugging, or building on prior work, inspect related workspaces
|
||||
before making changes.
|
||||
|
||||
Available commands:
|
||||
|
||||
ctask list --all discover all workspaces, including archived
|
||||
ctask info <workspace> view metadata and status of any workspace
|
||||
ctask notes <workspace> read another workspace's notes.md
|
||||
ctask path <workspace> get the filesystem path to inspect files directly
|
||||
|
||||
Treat other workspaces as read-only unless the user explicitly asks you to
|
||||
modify them.
|
||||
|
||||
## Session Handoff
|
||||
|
||||
Before ending a session, append a brief summary to notes.md with:
|
||||
|
||||
- What was accomplished
|
||||
- Key decisions made
|
||||
- Open follow-ups or unfinished work
|
||||
- How to continue from here
|
||||
|
||||
Keep it concise -- a few bullet points is enough.
|
||||
`
|
||||
}
|
||||
|
||||
// ClaudeMDProject returns the built-in default CLAUDE.md content for a project workspace.
|
||||
func ClaudeMDProject() string {
|
||||
return `# Project Workspace Guidelines
|
||||
|
||||
This is a ctask project workspace -- a long-lived working environment, not a disposable task.
|
||||
|
||||
## Workspace Structure
|
||||
|
||||
This is a ctask project workspace. The workspace root contains ctask management
|
||||
files. Your project code lives in the project subdirectory.
|
||||
|
||||
- Workspace root: ctask metadata, session logs, notes, reference material
|
||||
- Project subdirectory: source code, project CLAUDE.md, project configuration
|
||||
- ` + "`context/`" + `: reference material and imported specs (workspace level)
|
||||
- ` + "`output/`" + `: deliverables and exports (workspace level)
|
||||
- ` + "`logs/`" + `: session logs (managed by ctask)
|
||||
|
||||
When working on project code, operate inside the project subdirectory.
|
||||
Place project-specific CLAUDE.md, documentation, and configuration there.
|
||||
|
||||
## File Placement
|
||||
|
||||
- Source code -> ` + "`src/`" + ` (inside the project subdirectory)
|
||||
- Documentation -> ` + "`docs/`" + ` (workspace level for general notes, project subdir for project docs)
|
||||
- Deliverables and exports -> ` + "`output/`" + ` (workspace level)
|
||||
- Reference material -> ` + "`context/`" + ` (workspace level)
|
||||
- Tests -> ` + "`tests/`" + ` (inside the project subdirectory)
|
||||
- Configuration files -> inside the project subdirectory
|
||||
- Do not place non-code outputs in the workspace root
|
||||
|
||||
## Conventions
|
||||
|
||||
- This project uses git. Commit meaningful changes with clear messages.
|
||||
- Do not install global packages or modify system files unless asked.
|
||||
- Record important assumptions and actions in notes.md (workspace level).
|
||||
- Keep the workspace root clean.
|
||||
|
||||
## Git
|
||||
|
||||
This workspace uses a single git repository initialized at the workspace root.
|
||||
The project subdirectory and all its contents are tracked by this root repo.
|
||||
Do not initialize additional git repositories inside the project subdirectory
|
||||
or any other subdirectory. If you need to check git status or make commits,
|
||||
the repo root is the workspace root.
|
||||
|
||||
## Cross-Workspace Context
|
||||
|
||||
Related work may exist in other ctask workspaces. For project continuation,
|
||||
migration, debugging, or building on prior work, inspect related workspaces
|
||||
before making changes.
|
||||
|
||||
Available commands:
|
||||
|
||||
ctask list --all discover all workspaces, including archived
|
||||
ctask info <workspace> view metadata and status of any workspace
|
||||
ctask notes <workspace> read another workspace's notes.md
|
||||
ctask path <workspace> get the filesystem path to inspect files directly
|
||||
|
||||
Treat other workspaces as read-only unless the user explicitly asks you to
|
||||
modify them.
|
||||
|
||||
## Session Handoff
|
||||
|
||||
Before ending a session, append a brief summary to notes.md with:
|
||||
|
||||
- What was accomplished
|
||||
- Key decisions made
|
||||
- Open follow-ups or unfinished work
|
||||
- How to continue from here
|
||||
`
|
||||
}
|
||||
|
||||
// NotesMD returns the skeleton notes.md content.
|
||||
func NotesMD(title string) string {
|
||||
return fmt.Sprintf(`# %s
|
||||
|
||||
@@ -1,128 +1,9 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClaudeMDIsASCII(t *testing.T) {
|
||||
content := ClaudeMD("test-slug", "general", "/tmp/test")
|
||||
for i, b := range []byte(content) {
|
||||
if b > 127 {
|
||||
t.Errorf("non-ASCII byte 0x%02x at position %d in CLAUDE.md template", b, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDContainsV03Sections(t *testing.T) {
|
||||
content := ClaudeMD("ignored", "ignored", "ignored")
|
||||
for _, want := range []string{
|
||||
"# Workspace Guidelines",
|
||||
"## File Placement",
|
||||
"Source code and scripts",
|
||||
"Documentation, summaries, reports",
|
||||
"Deliverables and exports",
|
||||
"Reference material and imported files",
|
||||
"## Conventions",
|
||||
"## Session Handoff",
|
||||
"What was accomplished",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("CLAUDE.md missing required section: %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDProjectIsASCII(t *testing.T) {
|
||||
content := ClaudeMDProject()
|
||||
for i, b := range []byte(content) {
|
||||
if b > 127 {
|
||||
t.Errorf("non-ASCII byte 0x%02x at position %d in project CLAUDE.md template", b, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDProjectContainsRequiredSections(t *testing.T) {
|
||||
content := ClaudeMDProject()
|
||||
for _, want := range []string{
|
||||
"# Project Workspace Guidelines",
|
||||
"long-lived working environment",
|
||||
"## File Placement",
|
||||
"Source code -> ",
|
||||
"Tests -> ",
|
||||
"## Conventions",
|
||||
"This project uses git",
|
||||
"## Session Handoff",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("project CLAUDE.md missing required section: %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDProjectContainsNestedGitRule(t *testing.T) {
|
||||
body := ClaudeMDProject()
|
||||
for _, must := range []string{
|
||||
"single git repository",
|
||||
"Do not initialize additional git repositories",
|
||||
} {
|
||||
if !strings.Contains(body, must) {
|
||||
t.Errorf("ClaudeMDProject missing guidance %q", must)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDProjectDescribesWorkspaceStructure(t *testing.T) {
|
||||
body := ClaudeMDProject()
|
||||
for _, must := range []string{
|
||||
"## Workspace Structure",
|
||||
"Workspace root: ctask metadata",
|
||||
"Project subdirectory",
|
||||
"project CLAUDE.md",
|
||||
"operate inside the project subdirectory",
|
||||
} {
|
||||
if !strings.Contains(body, must) {
|
||||
t.Errorf("project CLAUDE.md missing marker %q", must)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDContainsCrossWorkspaceSection(t *testing.T) {
|
||||
// v0.5.2: both templates teach agents to use ctask's read-only context
|
||||
// commands before starting work.
|
||||
content := ClaudeMD("ignored", "ignored", "ignored")
|
||||
for _, must := range []string{
|
||||
"## Cross-Workspace Context",
|
||||
"ctask list --all",
|
||||
"ctask info <workspace>",
|
||||
"ctask notes <workspace>",
|
||||
"ctask path <workspace>",
|
||||
"Treat other workspaces as read-only",
|
||||
} {
|
||||
if !strings.Contains(content, must) {
|
||||
t.Errorf("task CLAUDE.md missing cross-workspace marker %q", must)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeMDProjectContainsCrossWorkspaceSection(t *testing.T) {
|
||||
content := ClaudeMDProject()
|
||||
for _, must := range []string{
|
||||
"## Cross-Workspace Context",
|
||||
"ctask list --all",
|
||||
"ctask info <workspace>",
|
||||
"ctask notes <workspace>",
|
||||
"ctask path <workspace>",
|
||||
"Treat other workspaces as read-only",
|
||||
} {
|
||||
if !strings.Contains(content, must) {
|
||||
t.Errorf("project CLAUDE.md missing cross-workspace marker %q", must)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotesMDIsASCII(t *testing.T) {
|
||||
content := NotesMD("test title")
|
||||
for i, b := range []byte(content) {
|
||||
|
||||
@@ -88,12 +88,16 @@ func Create(opts CreateOpts) (*CreateResult, error) {
|
||||
return nil, fmt.Errorf("creating workspace dir: %w", err)
|
||||
}
|
||||
|
||||
// Standard subdirectories
|
||||
for _, sub := range []string{"context", "output", "logs"} {
|
||||
// Standard subdirectories. v0.6: context/notes-archive/ is created
|
||||
// (and pinned by .gitkeep) so the notes-archival convention in
|
||||
// AGENTS.md has a ready destination directory.
|
||||
for _, sub := range []string{"context", "context/notes-archive", "output", "logs"} {
|
||||
if err := os.MkdirAll(filepath.Join(wsDir, sub), 0755); err != nil {
|
||||
return nil, fmt.Errorf("creating %s dir: %w", sub, err)
|
||||
}
|
||||
}
|
||||
gitkeepPath := filepath.Join(wsDir, "context", "notes-archive", ".gitkeep")
|
||||
_ = os.WriteFile(gitkeepPath, nil, 0644)
|
||||
|
||||
// Adjust title if slug was suffixed due to collision
|
||||
actualTitle := title
|
||||
@@ -108,7 +112,7 @@ func Create(opts CreateOpts) (*CreateResult, error) {
|
||||
}
|
||||
|
||||
// Layer 1: built-in defaults (workspace is brand new, so this always writes them).
|
||||
writeBuiltinDefaults(wsDir, actualTitle, opts.IsProject)
|
||||
writeBuiltinDefaults(wsDir, actualTitle, opts.IsProject, opts.AgentSpec.Type)
|
||||
|
||||
// Layer 2: general user seed (overwrites built-in defaults; skips task.yaml/.ctask).
|
||||
if opts.SeedDir != "" {
|
||||
@@ -168,19 +172,26 @@ func Create(opts CreateOpts) (*CreateResult, error) {
|
||||
return &CreateResult{Path: wsDir, Meta: meta}, nil
|
||||
}
|
||||
|
||||
// writeBuiltinDefaults writes the built-in CLAUDE.md and notes.md for a new workspace.
|
||||
// These files are unconditionally replaceable by the seed overlay layers.
|
||||
func writeBuiltinDefaults(wsDir, title string, isProject bool) {
|
||||
claudePath := filepath.Join(wsDir, "CLAUDE.md")
|
||||
var claudeBody string
|
||||
if isProject {
|
||||
claudeBody = seed.ClaudeMDProject()
|
||||
} else {
|
||||
// The slug/category/path args are kept for API compatibility but are
|
||||
// not interpolated by the v0.3 template.
|
||||
claudeBody = seed.ClaudeMD("", "", wsDir)
|
||||
// writeBuiltinDefaults writes the v0.6 built-in workspace files for a new
|
||||
// workspace: AGENTS.md (always), CLAUDE.md shim (claude type only),
|
||||
// handoff.md (always), and notes.md (always). All four are unconditionally
|
||||
// replaceable by the seed overlay layers — a user seed dir's AGENTS.md or
|
||||
// CLAUDE.md wins.
|
||||
//
|
||||
// The CLAUDE.md shim is generated only for agentType == "claude" per v0.6
|
||||
// spec §6 (the opencode shim is deferred until its instruction-file
|
||||
// convention is verified).
|
||||
func writeBuiltinDefaults(wsDir, title string, isProject bool, agentType string) {
|
||||
agentsPath := filepath.Join(wsDir, "AGENTS.md")
|
||||
_ = os.WriteFile(agentsPath, []byte(seed.AgentsMD(isProject)), 0644)
|
||||
|
||||
if agentType == "claude" {
|
||||
claudePath := filepath.Join(wsDir, "CLAUDE.md")
|
||||
_ = os.WriteFile(claudePath, []byte(seed.ClaudeShimMD()), 0644)
|
||||
}
|
||||
_ = os.WriteFile(claudePath, []byte(claudeBody), 0644)
|
||||
|
||||
handoffPath := filepath.Join(wsDir, "handoff.md")
|
||||
_ = os.WriteFile(handoffPath, []byte(seed.HandoffMD()), 0644)
|
||||
|
||||
notesPath := filepath.Join(wsDir, "notes.md")
|
||||
_ = os.WriteFile(notesPath, []byte(seed.NotesMD(title)), 0644)
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestCreateWorkspaceNoTitle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectWritesProjectClaudeMD(t *testing.T) {
|
||||
func TestCreateProjectWritesAgentsMDAndShim(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
opts := CreateOpts{
|
||||
Root: root,
|
||||
@@ -103,20 +103,26 @@ func TestCreateProjectWritesProjectClaudeMD(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(ws.Path, "CLAUDE.md"))
|
||||
agentsBody, err := os.ReadFile(filepath.Join(ws.Path, "AGENTS.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read AGENTS.md: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(agentsBody), "Workspace Structure (project workspace)") {
|
||||
t.Errorf("project AGENTS.md missing project-structure section, got:\n%s", agentsBody)
|
||||
}
|
||||
claudeBody, err := os.ReadFile(filepath.Join(ws.Path, "CLAUDE.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read CLAUDE.md: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
if !strings.Contains(content, "# Project Workspace Guidelines") {
|
||||
t.Errorf("expected project CLAUDE.md, got:\n%s", content)
|
||||
if !strings.Contains(string(claudeBody), "Read AGENTS.md first") {
|
||||
t.Errorf("CLAUDE.md is not the AGENTS.md shim, got:\n%s", claudeBody)
|
||||
}
|
||||
if ws.Meta.Type != "project" {
|
||||
t.Errorf("Type: got %q, want \"project\"", ws.Meta.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskWritesTaskClaudeMD(t *testing.T) {
|
||||
func TestCreateTaskWritesAgentsMDAndShim(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
opts := CreateOpts{
|
||||
Root: root,
|
||||
@@ -129,15 +135,94 @@ func TestCreateTaskWritesTaskClaudeMD(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
body, _ := os.ReadFile(filepath.Join(ws.Path, "CLAUDE.md"))
|
||||
if !strings.Contains(string(body), "# Workspace Guidelines") {
|
||||
t.Errorf("expected task CLAUDE.md, got:\n%s", string(body))
|
||||
agentsBody, _ := os.ReadFile(filepath.Join(ws.Path, "AGENTS.md"))
|
||||
if !strings.Contains(string(agentsBody), "File Placement (task workspace)") {
|
||||
t.Errorf("task AGENTS.md missing task file-placement section, got:\n%s", agentsBody)
|
||||
}
|
||||
claudeBody, _ := os.ReadFile(filepath.Join(ws.Path, "CLAUDE.md"))
|
||||
if !strings.Contains(string(claudeBody), "Read AGENTS.md first") {
|
||||
t.Errorf("CLAUDE.md is not the AGENTS.md shim, got:\n%s", claudeBody)
|
||||
}
|
||||
if ws.Meta.Type != "task" {
|
||||
t.Errorf("Type: got %q, want \"task\"", ws.Meta.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWritesAgentsMDAlways(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
res, err := Create(CreateOpts{
|
||||
Root: tmp,
|
||||
Title: "agents-md-test",
|
||||
Category: "general",
|
||||
AgentSpec: AgentSpec{Type: "opencode"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(res.Path, "AGENTS.md")); err != nil {
|
||||
t.Errorf("AGENTS.md missing: %v", err)
|
||||
}
|
||||
// No CLAUDE.md for opencode in v0.6.
|
||||
if _, err := os.Stat(filepath.Join(res.Path, "CLAUDE.md")); !os.IsNotExist(err) {
|
||||
t.Errorf("CLAUDE.md should NOT exist for opencode (got %v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWritesHandoffAndContextArchive(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
res, err := Create(CreateOpts{
|
||||
Root: tmp,
|
||||
Title: "scaffold-test",
|
||||
Category: "general",
|
||||
AgentSpec: AgentSpec{Type: "claude"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(res.Path, "handoff.md")); err != nil {
|
||||
t.Errorf("handoff.md missing: %v", err)
|
||||
}
|
||||
archiveDir := filepath.Join(res.Path, "context", "notes-archive")
|
||||
if info, err := os.Stat(archiveDir); err != nil || !info.IsDir() {
|
||||
t.Errorf("context/notes-archive/ missing or not a dir: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(archiveDir, ".gitkeep")); err != nil {
|
||||
t.Errorf(".gitkeep missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDoesNotModifyExistingWorkspace(t *testing.T) {
|
||||
// Pin the no-retroactive-modification invariant: a pre-v0.6 workspace
|
||||
// that already exists on disk must not acquire AGENTS.md, handoff.md,
|
||||
// or context/notes-archive/ from any read-side code path.
|
||||
tmp := t.TempDir()
|
||||
wsDir := filepath.Join(tmp, "general", "2026-01-01_legacy-ws")
|
||||
if err := os.MkdirAll(wsDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
yaml := []byte("id: 20260101-000000\nslug: legacy-ws\ntitle: legacy\n" +
|
||||
"created_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\n" +
|
||||
"status: active\ncategory: general\ntype: task\nmode: local\nagent: claude\n")
|
||||
if err := os.WriteFile(filepath.Join(wsDir, "task.yaml"), yaml, 0644); err != nil {
|
||||
t.Fatalf("write task.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Read it back through ReadMeta — the path every other ctask command
|
||||
// uses. A read must not trigger any writes to the workspace.
|
||||
if _, err := ReadMeta(filepath.Join(wsDir, "task.yaml")); err != nil {
|
||||
t.Fatalf("ReadMeta: %v", err)
|
||||
}
|
||||
|
||||
for _, file := range []string{"AGENTS.md", "handoff.md"} {
|
||||
if _, err := os.Stat(filepath.Join(wsDir, file)); !os.IsNotExist(err) {
|
||||
t.Errorf("legacy workspace acquired %s after ReadMeta (got %v)", file, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(wsDir, "context", "notes-archive")); !os.IsNotExist(err) {
|
||||
t.Errorf("legacy workspace acquired context/notes-archive/ after ReadMeta (got %v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAppliesGeneralSeed(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
seedDir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user