Skip to content

Unify persona contract in AGENTS.md#749

Merged
jolestar merged 1 commit intomainfrom
refactor/persona-contract-single-file
Mar 10, 2026
Merged

Unify persona contract in AGENTS.md#749
jolestar merged 1 commit intomainfrom
refactor/persona-contract-single-file

Conversation

@jolestar
Copy link
Collaborator

@jolestar jolestar commented Mar 9, 2026

Summary

  • unify agent-home persona contract around a single canonical AGENTS.md
  • remove ROLE.md, IDENTITY.md, and SOUL.md from built-in templates and runtime parsing
  • make serve derive controller role from AGENTS.md front matter and update docs/contracts accordingly

Testing

  • GOCACHE=$(pwd)/.gocache go test ./pkg/agenthome ./pkg/prompt
  • GOCACHE=$(pwd)/.gocache go test ./cmd/holon -run "Test(ControllerRoleLabelForPersona|LoadControllerRole|LoadControllerRole_EmptyFile|BuildServeStartupDiagnostics_SubscriptionRPCOnly|WriteServeStartupDiagnostics|ControllerPrompts_IncludeAgentHomeContract)$"

Notes

  • local untracked docs were intentionally left out of this PR

Copilot AI review requested due to automatic review settings March 9, 2026 04:28
@cloudflare-workers-and-pages
Copy link

Deploying holon with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9d14338
Status: ✅  Deploy successful!
Preview URL: https://21e05ba2.holon-1dl.pages.dev
Branch Preview URL: https://refactor-persona-contract-si.holon-1dl.pages.dev

View logs

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holon Ready Ready Preview, Comment Mar 9, 2026 4:28am

Copy link
Contributor

@holonbot holonbot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #749 - Unify persona contract in AGENTS.md

Summary

This PR consolidates the multi-file persona contract (AGENTS.md, ROLE.md, IDENTITY.md, SOUL.md) into a single canonical AGENTS.md file with structured YAML front matter. The refactoring touches 26 files across the codebase, updating templates, runtime loading logic, tests, and documentation.

Overall Assessment: LGTM with minor suggestions

The changes are well-executed, properly tested, and align with the stated design goal of unifying the persona contract around a single source of truth. The refactoring maintains backward compatibility considerations and includes thoughtful cleanup of legacy files.


Key Findings

1. Core Implementation: LoadPersona() - Well Designed

The new LoadPersona() function in pkg/agenthome/agenthome.go is a significant improvement over the previous ad-hoc role inference:

Strengths:

  • Proper YAML front matter parsing with clear validation
  • Explicit contract version checking (persona_contract: v2)
  • Structured error messages that guide users toward correct format
  • Role normalization with explicit whitelist (executor, github_solver, pm, autonomous)

The validation logic is comprehensive:

  • Requires front matter delimiters (---\n)
  • Validates contract version
  • Validates role against whitelist
  • Ensures non-empty body

Observation:
The role whitelist approach is good for consistency. The mapping to controller labels (pm/dev) is clear:

case role == "executor", role == "github_solver": return "dev"
case role == "pm", role == "autonomous": return "pm"

2. Template Updates - Consistent and Complete

All three templates (default, github-solver, autonomous) were updated to include proper front matter. The structure is consistent:

---
persona_contract: v2
role: <executor|github_solver|autonomous>
---

The content consolidation from the separate files is well done - key information from IDENTITY.md and SOUL.md has been incorporated into Identity, Values, and Failure Policy sections within AGENTS.md.

3. Cleanup of Legacy Files - Thoughtful

The --force flag behavior now removes legacy persona files (ROLE.md, IDENTITY.md, SOUL.md):

if opts.Force {
    for _, legacyName := range []string{"ROLE.md", "IDENTITY.md", "SOUL.md"} {
        legacyPath := filepath.Join(agentHome, legacyName)
        if err := os.Remove(legacyPath); err != nil && !os.IsNotExist(err) {
            return fmt.Errorf("failed to remove legacy persona file %s: %w", legacyPath, err)
        }
    }
}

This is a good migration strategy - it doesn't break existing setups but provides a clean path forward for users who explicitly opt into reinitialization.

4. Serve Role Loading - Correct Refactoring

The loadControllerRole() function now properly delegates to agenthome.LoadPersona() instead of doing its own file reading and fuzzy inference. This eliminates the complex inferRoleFromFrontMatter() and inferControllerRole() logic, replacing it with a clean mapping from normalized persona roles.

The test updates in serve_test.go are appropriate, updating expectations to use AGENTS.md as the role source.

5. Documentation Updates - Thorough

Documentation changes across multiple files consistently reflect the new single-file model:

  • AGENTS.md, README.md - high-level descriptions
  • docs/agent-home-persona-redesign.md - design documentation
  • docs/agent-home-unification.md - architecture docs
  • docs/architecture-current.md, docs/development.md, docs/serve-webhook.md - specific domain docs

The consistency here is good - users will encounter a coherent mental model regardless of which documentation they read.

6. Test Coverage - Good

The test additions in agenthome_test.go are comprehensive:

  • TestLoadPersona() - happy path validation
  • TestLoadPersona_RejectsInvalidFrontMatter() - covers all error cases (missing front matter, missing version, unsupported role, empty body)

The test updates in serve_test.go correctly adapt to the new loading mechanism.


Minor Suggestions

Suggestion 1: Empty Body Error Message Clarity (Nit)

File: pkg/agenthome/agenthome.go:439

The error message for empty body references "closing YAML front matter delimiter" which might be confusing since the actual issue is the empty body, not a missing delimiter.

if body == "" {
    return Persona{}, fmt.Errorf("persona file body is empty: %s", personaPath)
}

The error is technically correct but could be clearer:

return Persona{}, fmt.Errorf("persona file has empty body after front matter: %s", personaPath)

This is minor - the current message is acceptable.

Suggestion 2: Contract Version String Literal (Nit)

File: pkg/agenthome/agenthome.go:446

The contract version check uses a string literal:

if strings.TrimSpace(frontMatter.ContractVersion) != "v2" {

Consider defining a constant for this:

const PersonaContractVersion = "v2"

This would make future version upgrades easier to track. However, since this is currently the only version and it's tied to front matter schema, the literal is acceptable for now.


Security & Safety

No security concerns identified. The changes are purely refactoring with no new attack surfaces:

  • File I/O is limited to agent home directory
  • YAML parsing is constrained to a small, trusted front matter section
  • Input validation is robust
  • No external dependencies added

Backward Compatibility

The PR handles backward compatibility thoughtfully:

  1. CLAUDE.md compatibility pointer is preserved
  2. Legacy files are not auto-deleted without explicit --force flag
  3. Existing agent homes will continue working until users explicitly reinitialize

One consideration for documentation: users upgrading should be informed that running holon agent init --force will remove their legacy persona files. This is already hinted at in the flag description, but could be worth a migration note in docs.


Performance

No performance concerns:

  • The new LoadPersona() is more efficient than the previous string-based inference
  • File I/O is minimized (single read vs multiple potential reads)
  • Parsing is trivial YAML with fixed schema

Conclusion

This is a clean, well-executed refactoring that simplifies the persona contract model while maintaining proper validation and error handling. The test coverage is good, documentation updates are thorough, and the migration strategy is respectful of existing users.

Recommendation: Approve

The suggestions above are minor and don't block merge. The core changes are solid and ready to land.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates the agent-home persona/role contract into a single canonical AGENTS.md (with YAML front matter), removing reliance on legacy ROLE.md / IDENTITY.md / SOUL.md and updating holon serve to infer controller role from AGENTS.md.

Changes:

  • Remove legacy persona files from built-in init templates and prompt contract text, and add v2 YAML front matter to template AGENTS.md.
  • Add agenthome.LoadPersona() to parse AGENTS.md front matter and body; update serve to derive controller role from it.
  • Update tests and documentation to reflect the unified AGENTS.md persona contract.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/prompt/compiler_test.go Asserts embedded contract no longer references legacy persona files.
pkg/prompt/assets/contracts/common.md Updates agent-home contract docs to make AGENTS.md the canonical persona contract.
pkg/agenthome/assets/templates/github-solver/SOUL.md Removes legacy template file.
pkg/agenthome/assets/templates/github-solver/ROLE.md Removes legacy template file.
pkg/agenthome/assets/templates/github-solver/IDENTITY.md Removes legacy template file.
pkg/agenthome/assets/templates/github-solver/AGENTS.md Adds v2 front matter + folds identity/values into AGENTS.md.
pkg/agenthome/assets/templates/default/SOUL.md Removes legacy template file.
pkg/agenthome/assets/templates/default/ROLE.md Removes legacy template file.
pkg/agenthome/assets/templates/default/IDENTITY.md Removes legacy template file.
pkg/agenthome/assets/templates/default/AGENTS.md Adds v2 front matter + folds identity/values into AGENTS.md.
pkg/agenthome/assets/templates/autonomous/SOUL.md Removes legacy template file.
pkg/agenthome/assets/templates/autonomous/ROLE.md Removes legacy template file.
pkg/agenthome/assets/templates/autonomous/IDENTITY.md Removes legacy template file.
pkg/agenthome/assets/templates/autonomous/AGENTS.md Adds v2 front matter + folds identity/values into AGENTS.md.
pkg/agenthome/agenthome_test.go Updates layout/template tests; adds LoadPersona parsing tests.
pkg/agenthome/agenthome.go Introduces Persona + LoadPersona; removes legacy files on --force.
docs/serve-webhook.md Updates serve diagnostics documentation to reference AGENTS.md.
docs/development.md Updates runtime persona guidance to reference AGENTS.md only.
docs/architecture-current.md Updates architecture notes to reflect unified persona contract.
docs/agent-home-unification.md Updates unification design doc to remove legacy persona files.
docs/agent-home-persona-redesign.md Updates persona redesign doc to reflect AGENTS.md contract.
cmd/holon/serve_test.go Updates role inference tests to use AGENTS.md/persona role mapping.
cmd/holon/serve.go Switches controller role inference to agenthome.LoadPersona() and AGENTS.md.
cmd/holon/agent.go Updates --force help text to match new behavior.
README.md Updates agent-home model description to reference canonical AGENTS.md.
AGENTS.md Updates repo-level notes to reference canonical agent-home AGENTS.md.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

}
if opts.Force {
for _, legacyName := range []string{"ROLE.md", "IDENTITY.md", "SOUL.md"} {
legacyPath := filepath.Join(agentHome, legacyName)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When --force is used, legacy persona files are removed via os.Remove without checking the path type. If a legacy path exists but is a directory or otherwise non-regular (similar to the ensureFile/writeFile checks), EnsureLayoutWithOptions will fail unexpectedly. Consider stat'ing first and only removing regular files (or providing a clearer error/behavior for non-regular paths).

Suggested change
legacyPath := filepath.Join(agentHome, legacyName)
legacyPath := filepath.Join(agentHome, legacyName)
info, err := os.Stat(legacyPath)
if err != nil {
if os.IsNotExist(err) {
continue
}
return fmt.Errorf("failed to stat legacy persona path %s: %w", legacyPath, err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("legacy persona path %s exists but is not a regular file", legacyPath)
}

Copilot uses AI. Check for mistakes.
- `role_source`: Always `${agent_home}/ROLE.md` (single source of truth)
- `role_inferred`: Role inferred from `ROLE.md` content (`pm`/`dev`)
- `role_source`: Always `${agent_home}/AGENTS.md` (single source of truth)
- `role_inferred`: Controller role derived from `AGENTS.md` front matter (`pm`/`dev`)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs say role_inferred is derived from AGENTS.md front matter values pm/dev, but the v2 templates use persona roles like executor, github_solver, and autonomous in front matter, with holon serve mapping those to controller labels (dev/pm). Please update this line to reflect the actual front matter role values and the mapping logic.

Suggested change
- `role_inferred`: Controller role derived from `AGENTS.md` front matter (`pm`/`dev`)
- `role_inferred`: Controller role label (`dev`/`pm`) inferred by `holon serve` from `AGENTS.md` persona front matter (e.g. `executor`, `github_solver`, `autonomous`)

Copilot uses AI. Check for mistakes.
{
name: "empty body",
content: "---\npersona_contract: v2\nrole: executor\n---\n",
wantErr: "closing YAML front matter delimiter",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "empty body" test case currently expects an error about a missing closing front-matter delimiter, but the input does include a closing --- line. This expectation is coupled to the current delimiter search behavior in LoadPersona and conflicts with the later body is empty validation in LoadPersona. After fixing front-matter parsing (e.g., allowing the closing delimiter at EOF), this test should assert the body-empty error instead.

Suggested change
wantErr: "closing YAML front matter delimiter",
wantErr: "body is empty",

Copilot uses AI. Check for mistakes.
Comment on lines +279 to +294
content := strings.TrimSpace(string(data))
if content == "" {
return Persona{}, fmt.Errorf("persona file is empty: %s", personaPath)
}
if !strings.HasPrefix(content, "---\n") {
return Persona{}, fmt.Errorf("persona file %s must start with YAML front matter", personaPath)
}

rest := strings.TrimPrefix(content, "---\n")
idx := strings.Index(rest, "\n---\n")
if idx < 0 {
return Persona{}, fmt.Errorf("persona file %s must contain a closing YAML front matter delimiter", personaPath)
}

frontMatterRaw := rest[:idx]
body := strings.TrimSpace(rest[idx+len("\n---\n"):])
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadPersona trims the entire file (TrimSpace) and then searches for the closing front-matter delimiter using the exact substring "\n---\n". This makes the parser fail to recognize a valid closing delimiter when it appears at EOF (e.g. front matter ends with ---\n and no body) and also breaks on CRLF (\r\n) line endings. Consider parsing by lines (treat a line equal to --- as the delimiter), and/or normalizing \r\n to \n, and avoid trimming in a way that can remove the trailing newline needed for delimiter detection.

Suggested change
content := strings.TrimSpace(string(data))
if content == "" {
return Persona{}, fmt.Errorf("persona file is empty: %s", personaPath)
}
if !strings.HasPrefix(content, "---\n") {
return Persona{}, fmt.Errorf("persona file %s must start with YAML front matter", personaPath)
}
rest := strings.TrimPrefix(content, "---\n")
idx := strings.Index(rest, "\n---\n")
if idx < 0 {
return Persona{}, fmt.Errorf("persona file %s must contain a closing YAML front matter delimiter", personaPath)
}
frontMatterRaw := rest[:idx]
body := strings.TrimSpace(rest[idx+len("\n---\n"):])
// Normalize line endings so parsing works consistently across platforms.
rawContent := string(data)
normalized := strings.ReplaceAll(rawContent, "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r", "\n")
if strings.TrimSpace(normalized) == "" {
return Persona{}, fmt.Errorf("persona file is empty: %s", personaPath)
}
lines := strings.Split(normalized, "\n")
if len(lines) == 0 || lines[0] != "---" {
return Persona{}, fmt.Errorf("persona file %s must start with YAML front matter", personaPath)
}
closingIdx := -1
for i := 1; i < len(lines); i++ {
if lines[i] == "---" {
closingIdx = i
break
}
}
if closingIdx < 0 {
return Persona{}, fmt.Errorf("persona file %s must contain a closing YAML front matter delimiter", personaPath)
}
frontMatterRaw := strings.Join(lines[1:closingIdx], "\n")
body := strings.TrimSpace(strings.Join(lines[closingIdx+1:], "\n"))

Copilot uses AI. Check for mistakes.
@jolestar jolestar merged commit fc7368d into main Mar 10, 2026
20 checks passed
@jolestar jolestar deleted the refactor/persona-contract-single-file branch March 10, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants