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:
2026-05-15 11:15:16 -04:00
parent a61f900c86
commit 0c6ed0c0cf
6 changed files with 322 additions and 268 deletions
+130
View File
@@ -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
`
}
+72
View File
@@ -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
}
}
}
}
-125
View File
@@ -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
-119
View File
@@ -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) {