Documentation
ΒΆ
Index ΒΆ
Constants ΒΆ
const CanaryEnd = "END"
const CanaryKey = "CANARY"
const CanaryStart = "START"
const CommentPrefix = "<!--"
const CommentSuffix = "-->"
Variables ΒΆ
var ( // Public unnamed section markers (if needed externally for docs) StartMarker = gate.StartMarker("", markdownOptions...) EndMarker = gate.EndMarker("", markdownOptions...) )
Precomputed Start/End markers for unnamed CANARY section
var InitCmd = &cobra.Command{ Use: "init [project-name]", Short: "Initialize a new project with CANARY", Long: `Bootstrap a new project with CANARY spec-kit-inspired workflow. Installation Modes: Global (default): Installs commands in ~/.claude/commands/, ~/.cursor/commands/, etc. for use across all projects Local (--local): Installs commands in .claude/commands/, .cursor/commands/, etc. for project-specific use Creates: - .canary/ directory with templates, scripts, agents, and slash commands - .canary/agents/ directory with pre-configured CANARY agent definitions - README.md with CANARY token format specification - GAP_ANALYSIS.md template for tracking requirements - CLAUDE.md for AI agent integration (slash commands) The agent files support template variables that can be customized: --agent-prefix: Agent name prefix (default: project key) --agent-model: AI model to use (default: sonnet) --agent-color: Agent color theme (default: blue) Examples: canary init # Global install (default) canary init --local # Local install in current project canary init myproject --local # Local install in new project canary init --force # Overwrite files you have customized (keeps a .bak) Re-running init never overwrites a file you have edited: differing files are kept and reported. Pass --force to replace them; the previous content is saved alongside as <file>.bak.`, RunE: func(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") projectName := "." if len(args) > 0 { projectName = args[0] } canaryDir := filepath.Join(projectName, ".canary") isUpdate := false if _, err := os.Stat(canaryDir); err == nil { isUpdate = true fmt.Println("π¦ Existing CANARY project detected - updating...") } if projectName != "." { if err := os.MkdirAll(projectName, 0750); err != nil { return fmt.Errorf("create project dir: %w", err) } } projectKey, _ := cmd.Flags().GetString("key") projectYamlPath := filepath.Join(projectName, ".canary", "project.yaml") if isUpdate && projectKey == "" { if existingContent, err := os.ReadFile(projectYamlPath); err == nil { for _, line := range strings.Split(string(existingContent), "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "key:") { parts := strings.SplitN(trimmed, ":", 2) if len(parts) == 2 { existingKey := strings.TrimSpace(parts[1]) existingKey = strings.Trim(existingKey, "\"' ") if existingKey != "" && existingKey != "{{PROJECT_KEY}}" { projectKey = existingKey fmt.Printf("π¦ Using existing project key: %s\n", projectKey) break } } } } } else { fmt.Printf("β οΈ Warning: Could not read project.yaml: %v\n", err) } } if projectKey == "" { fmt.Print("Enter project requirement ID prefix (e.g., CBIN, PROJ, ACME): ") var input string if _, err := fmt.Scanln(&input); err != nil { input = "" } projectKey = strings.TrimSpace(strings.ToUpper(input)) } if projectKey == "" { projectKey = "PROJ" } localInstall, _ := cmd.Flags().GetBool("local") agentsList, _ := cmd.Flags().GetStringSlice("agents") allAgents, _ := cmd.Flags().GetBool("all-agents") agentPrefix, _ := cmd.Flags().GetString("agent-prefix") agentModel, _ := cmd.Flags().GetString("agent-model") agentColor, _ := cmd.Flags().GetString("agent-color") if agentPrefix == "" { agentPrefix = projectKey } if agentModel == "" { agentModel = "sonnet" } if agentColor == "" { agentColor = "blue" } j := &safewrite.Journal{} var slashCommandNotes []string applyErr := func() error { if err := copyCanaryStructure(projectName, force, j); err != nil { return fmt.Errorf("copy .canary structure: %w", err) } canaryignoreContent, err := utils.ReadEmbeddedFile("base/.canaryignore") if err == nil { canaryignorePath := filepath.Join(projectName, ".canaryignore") if _, err := writeManagedFile(projectName, canaryignorePath, canaryignoreContent, 0640, force, j); err != nil { return fmt.Errorf("write .canaryignore: %w", err) } } if err := customizeProjectYaml(projectYamlPath, projectName, projectKey, j); err != nil { return fmt.Errorf("customize project.yaml: %w", err) } if err := copyAndProcessAgentFiles(projectName, agentPrefix, agentModel, agentColor, force, j); err != nil { return fmt.Errorf("copy agent files: %w", err) } notes, err := installSlashCommands(projectName, agentsList, allAgents, localInstall, force, j) if err != nil { return fmt.Errorf("install slash commands: %w", err) } slashCommandNotes = notes if err := installAgentFilesToSystems(projectName, agentsList, allAgents, agentPrefix, agentModel, agentColor, localInstall, force, j); err != nil { return fmt.Errorf("install agent files to systems: %w", err) } if err := createCopilotInstructions(projectName, projectKey, j); err != nil { return fmt.Errorf("create Copilot instructions: %w", err) } readme := "# CANARY Token Specification\n\n" + "## Format\n\n" + "CANARY tokens track requirements directly in source code:\n\n" + "```\n" + "// CANARY: REQ=CBIN-###; FEATURE=\"Name\"; ASPECT=API; STATUS=IMPL; [TEST=TestName]; [BENCH=BenchName]; [OWNER=team]; UPDATED=<YYYY-MM-DD>\n" + "```\n\n" + "## Required Fields\n\n" + "- **REQ**: Requirement ID (format: CBIN-###)\n" + "- **FEATURE**: Short feature name\n" + "- **ASPECT**: Category (API, CLI, Engine, Storage, etc.)\n" + "- **STATUS**: Implementation state\n" + "- **UPDATED**: Last update date (YYYY-MM-DD)\n\n" + "## Status Values\n\n" + "- **MISSING**: Planned but not implemented\n" + "- **STUB**: Placeholder implementation\n" + "- **IMPL**: Implemented\n" + "- **TESTED**: Declared tested (add TEST= to declare it) β a declaration, not\n" + " proof; `canary verify` requires a passing evidence record for every\n" + " declared TEST= at the current commit\n" + "- **BENCHED**: Declared benchmarked (add BENCH= to declare it) β same caveat;\n" + " `canary verify` requires a bench evidence record for every declared BENCH=\n" + " at the current commit\n" + "- **REMOVED**: Deprecated/removed\n\n" + "## Optional Fields\n\n" + "- **TEST**: Test function name (declares TESTED; verified by evidence, not by the field alone)\n" + "- **BENCH**: Benchmark function name (declares BENCHED; verified by evidence, not by the field alone)\n" + "- **OWNER**: Team/person responsible\n\n" + "## Example\n\n" + "```go\n" + "// CANARY: REQ=CBIN-001; FEATURE=\"UserAuth\"; ASPECT=API; STATUS=TESTED; TEST=TestUserAuth; OWNER=backend; UPDATED=2025-10-16\n" + "func AuthenticateUser(credentials *Credentials) (*Session, error) {\n" + " // implementation\n" + "}\n" + "```\n\n" + "## Usage\n\n" + "```bash\n" + "# Scan for tokens and generate reports\n" + "canary scan --root . --out status.json --csv status.csv\n\n" + "# Produce evidence from a real test run, then verify GAP_ANALYSIS.md claims\n" + "# against it (STATUS=TESTED/BENCHED alone is a declaration, not proof).\n" + "# `canary evidence run` produces IMPORTED evidence (canary cannot know the\n" + "# command was a real test runner). Use run-go-test with an operator-named\n" + "# toolchain for EXECUTED evidence that `canary verify` accepts by default:\n" + "canary evidence run-go-test --project <KEY> --toolchain-path \"$(go env GOROOT)/bin/go\" -- -count=1 -json -bench=. -benchtime=1x ./... > evidence.json\n" + "canary evidence ingest --in evidence.json --out .canary/evidence.json\n" + "canary verify --root . --claims GAP_ANALYSIS.md\n\n" + "# Check for stale tokens (30-day threshold)\n" + "canary scan --root . --strict\n\n" + "# Report which TESTED/BENCHED tokens have stale evidence (reporting only; mutates nothing)\n" + "canary scan --root . --update-stale\n" + "```\n" readmePath := filepath.Join(projectName, "README_CANARY.md") if _, err := writeManagedFile(projectName, readmePath, []byte(readme), 0640, force, j); err != nil { return fmt.Errorf("write README: %w", err) } gap := "# Requirements Gap Analysis\n\n" + "## Claimed Requirements\n\n" + "List requirements that are fully implemented and verified:\n\n" + "β CBIN-001 - UserAuth API fully tested\n" + "β CBIN-002 - DataValidation with benchmarks\n\n" + "## Gaps\n\n" + "List requirements that are planned or in progress:\n\n" + "- [ ] CBIN-003 - ReportGeneration (STATUS=IMPL, needs tests)\n" + "- [ ] CBIN-004 - CacheOptimization (STATUS=STUB)\n\n" + "## Verification\n\n" + "Produce evidence from a real test run, then verify claims against it " + "(a declared STATUS=TESTED/BENCHED is not, on its own, proof). `canary\n" + "evidence run` produces IMPORTED evidence (canary cannot know the command\n" + "was a real test runner). Use run-go-test with an operator-named toolchain\n" + "for EXECUTED evidence that `canary verify` accepts by default:\n\n" + "```bash\n" + "canary evidence run-go-test --project <KEY> --toolchain-path \"$(go env GOROOT)/bin/go\" -- -count=1 -json -bench=. -benchtime=1x ./... > evidence.json\n" + "canary evidence ingest --in evidence.json --out .canary/evidence.json\n" + "canary verify --root . --claims GAP_ANALYSIS.md\n" + "```\n\n" + "This will:\n" + "- β Exit 0 if every claimed requirement has passing evidence for every\n" + " declared TEST=/BENCH= at the current commit\n" + "- β Exit 1 if a claim is overclaimed, unverified, or the claims file is\n" + " empty; a dirty working tree is also refused unless --allow-dirty is given\n" gapPath := filepath.Join(projectName, "GAP_ANALYSIS.md") if _, err := writeManagedFile(projectName, gapPath, []byte(gap), 0640, force, j); err != nil { return fmt.Errorf("write GAP_ANALYSIS.md: %w", err) } return nil }() if applyErr != nil { if rbErr := j.Rollback(); rbErr != nil { return fmt.Errorf("init failed: %w (rollback also failed: %v β inspect the tree)", applyErr, rbErr) } return fmt.Errorf("init failed: %w (all changes rolled back)", applyErr) } agentResults, err := updateAgentContextFiles(projectName, force, j) if err != nil { failed := 0 for _, r := range agentResults { if r.Action == "failed" { failed++ fmt.Printf(" β %s: %v\n", r.Path, r.Err) } else { fmt.Printf(" β %s (%s)\n", r.Path, r.Action) } } return fmt.Errorf("bootstrap succeeded but %d of %d agent-context file(s) failed (partial progress retained; failed files listed above): %w", failed, len(agentResults), err) } if isUpdate { fmt.Printf("\nβ Updated CANARY project in: %s\n\n", projectName) fmt.Println("Updated:") } else { fmt.Printf("\nβ Initialized CANARY project in: %s\n\n", projectName) fmt.Println("Created:") } fmt.Println(" β .canary/ - Full workflow structure") fmt.Println(" βββ agents/ - Pre-configured CANARY agent definitions") fmt.Println(" βββ memory/constitution.md - Project principles") fmt.Println(" βββ scripts/ - Automation scripts") fmt.Println(" βββ templates/ - Spec/plan templates") fmt.Println(" βββ templates/commands/ - Slash commands for AI agents") if localInstall { fmt.Println(" β Agent Files - Installed LOCALLY in project directory") } else { homeDir, _ := os.UserHomeDir() fmt.Printf(" β Agent Files - Installed GLOBALLY in %s\n", homeDir) } agentDirs := map[string]string{ ".claude": "Claude Code", ".cursor": "Cursor", ".github": "GitHub Copilot", ".windsurf": "Windsurf", ".kilocode": "Kilocode", ".roo": "Roo", ".opencode": "opencode", ".codex": "Codex", ".augment": "Auggie", ".codebuddy": "CodeBuddy", ".amazonq": "Amazon Q Developer", } checkDir := projectName if !localInstall { if homeDir, err := os.UserHomeDir(); err == nil { checkDir = homeDir } } installedAgents := []string{} for dir, name := range agentDirs { if _, err := os.Stat(filepath.Join(checkDir, dir)); err == nil { installedAgents = append(installedAgents, name) } } if len(installedAgents) > 0 { fmt.Printf(" β AI Agent Integration (%d systems configured):\n", len(installedAgents)) for _, agent := range installedAgents { fmt.Printf(" β’ %s\n", agent) } for _, note := range slashCommandNotes { fmt.Printf(" β’ %s\n", note) } } if !isUpdate { fmt.Println(" β README_CANARY.md - Token format specification") fmt.Println(" β GAP_ANALYSIS.md - Requirements tracking template") } agentFileLabels := map[string]string{ "AGENTS.md": "Codex / repository instructions", "CLAUDE.md": "Claude Code / Claude plugins", "CURSOR.md": "Cursor IDE / Cursor plugins", "copilot-instructions.md": "GitHub Copilot instructions", "AGENT_CONTEXT.md": "Full agent context reference", "canary-requirements.mdc": "Cursor rule (apply when editing requirements)", "mcp.json": "Optional MCP (run `canary mcp` then use Cursor MCP tools)", } for _, r := range agentResults { label := agentFileLabels[filepath.Base(r.Path)] if label == "" { fmt.Printf(" β %s (%s)\n", r.Path, r.Action) continue } fmt.Printf(" β %s - %s (%s)\n", r.Path, label, r.Action) } fmt.Print(` Available Slash Commands for AI Agents: /canary.constitution - Create/update project principles /canary.specify - Create requirement specification /canary.plan - Generate implementation plan /canary.scan - Scan for CANARY tokens /canary.verify - Verify GAP_ANALYSIS.md claims /canary.update-stale - Update stale tokens Next Steps: 1. Open in AI agent (Claude Code, Cursor, etc.) 2. Run: /canary.constitution to establish principles 3. Run: /canary.specify "your feature description" 4. Follow the spec-driven workflow! `) return nil }, }
InitCmd bootstraps a new project with CANARY token conventions
Functions ΒΆ
func CreateCopilotInstructions ΒΆ
CreateCopilotInstructions is an exported wrapper used by tests and higher-level callers outside the journaled init apply chain, so it runs unjournaled (nil journal).
Types ΒΆ
type AgentConfig ΒΆ
type AgentConfig struct {
Dir string // Directory for agent files
Prefix string // Prefix for command files (e.g., "canary.")
}
CANARY: REQ=ENG-4300; FEATURE="InitWorkflow"; ASPECT=CLI; STATUS=IMPL; OWNER=canary; UPDATED=2025-10-16 AgentConfig defines configuration for each supported AI agent
type AgentFileResult ΒΆ added in v0.3.6
type AgentFileResult struct {
Path string
Action string // "updated" | "created" | "kept" | "failed"
Err error
}
AgentFileResult records the outcome of one agent-context file updateAgentContextFiles attempted to write, so a caller can report exactly which files succeeded (and how -- created/updated/kept) and which failed, instead of only an aggregated error that hides successful partial progress. Err is non-nil if and only if Action is "failed".