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
1 change: 1 addition & 0 deletions go/cmd/compass/agent_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func renderConfigInfo(out io.Writer, msg *compassv1.GetAgentConfigInfoResponse)
{"mcp", msg.GetMcpServers()},
{"rules", msg.GetRules()},
{"subagents", msg.GetSubagents()},
{"prompts", msg.GetPrompts()},
}
for _, b := range buckets {
if _, err := fmt.Fprintf(out, "%s (%d):\n", b.label, len(b.names)); err != nil {
Expand Down
21 changes: 18 additions & 3 deletions go/cmd/compass/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
topDirSettings = "settings"
topDirRules = "rules"
topDirAgents = "agents"
topDirPrompts = "prompts"
)

// Top-level regular-file members admitted by exact filename, not under a top dir
Expand All @@ -42,6 +43,9 @@ const (
memberAgentsMD = "AGENTS.md"
memberModels = "models.yml"
settingsMember = "settings/config.yml"
// memberSystemMD is the only filename admitted under prompts/<role>/
// (store door: RIG-3075 T2).
memberSystemMD = "SYSTEM.md"
)

// maxBundleFileCount and maxBundleContentBytes are a fail-fast client-side check
Expand All @@ -67,6 +71,7 @@ var bundleTopDirs = map[string]bool{
topDirSettings: true,
topDirRules: true,
topDirAgents: true,
topDirPrompts: true,
}

// bundleNamePattern is the grammar for a member's <name> segment (store door:
Expand Down Expand Up @@ -128,7 +133,7 @@ func buildBundle(dir string) ([]byte, error) {
return nil, walkErr
}
if fileCount == 0 {
return nil, fmt.Errorf("bundle directory %q contains no members under skills/, extensions/, mcp/, settings/, rules/, or agents/ and no top-level %s or %s; use `agent-config delete` to clear the fleet config", dir, memberAgentsMD, memberModels)
return nil, fmt.Errorf("bundle directory %q contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/ and no top-level %s or %s; use `agent-config delete` to clear the fleet config", dir, memberAgentsMD, memberModels)
}
if err := tw.Close(); err != nil {
return nil, fmt.Errorf("finalizing tar: %w", err)
Expand Down Expand Up @@ -165,7 +170,7 @@ func validateDirMember(name string) error {
if !bundleTopDirs[parts[0]] {
return errNotWhitelisted(name)
}
if (parts[0] == topDirSkills || parts[0] == topDirExtensions) && len(parts) >= 2 && !bundleNamePattern.MatchString(parts[1]) {
if (parts[0] == topDirSkills || parts[0] == topDirExtensions || parts[0] == topDirPrompts) && len(parts) >= 2 && !bundleNamePattern.MatchString(parts[1]) {
return fmt.Errorf("bundle member name %q must match %s", parts[1], bundleNamePattern.String())
}
return nil
Expand All @@ -175,7 +180,7 @@ func validateDirMember(name string) error {
// neither under a whitelisted top dir nor one of the two top-level singletons
// (store door: configMemberParts).
func errNotWhitelisted(name string) error {
return fmt.Errorf("bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, or agents/ and is not a top-level %s or %s", name, memberAgentsMD, memberModels)
return fmt.Errorf("bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/ and is not a top-level %s or %s", name, memberAgentsMD, memberModels)
}

// addRegularMember validates a regular file against the door grammar and writes
Expand Down Expand Up @@ -277,6 +282,16 @@ func validateMemberGrammar(parts []string, name string, content []byte) error {
return fmt.Errorf("agents member %q must be flat agents/<name>.md", name)
}
return validateFlatNamedMember(topDirAgents, parts[1], name, ".md")
case topDirPrompts:
// Exactly prompts/<role>/SYSTEM.md — three components, grammar-valid
// <role>, filename exactly SYSTEM.md (store door: RIG-3075 T2).
if len(parts) != 3 || parts[2] != memberSystemMD {
return fmt.Errorf("prompts member %q must be prompts/<role>/%s", name, memberSystemMD)
}
if !bundleNamePattern.MatchString(parts[1]) {
return fmt.Errorf("prompts role name %q must match %s", parts[1], bundleNamePattern.String())
}
return nil
default:
// skills/ or extensions/: the <name> second component must match.
if len(parts) < 2 {
Expand Down
30 changes: 18 additions & 12 deletions go/cmd/compass/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func TestBuildBundleRejects(t *testing.T) {
{"bad agents ext", map[string]string{"agents/foo.txt": "x"}, "must end in one of .md"},
{"bad agents name", map[string]string{"agents/bad name.md": "x"}, "must match"},
{"nested agents", map[string]string{"agents/sub/foo.md": "x"}, "must be flat"},
{"prompts wrong filename", map[string]string{"prompts/supervisor/other.md": "x"}, "must be prompts/<role>/SYSTEM.md"},
{"prompts too deep", map[string]string{"prompts/supervisor/sub/SYSTEM.md": "x"}, "must be prompts/<role>/SYSTEM.md"},
{"prompts too shallow", map[string]string{"prompts/SYSTEM.md": "x"}, "must be prompts/<role>/SYSTEM.md"},
{"prompts bad role name", map[string]string{"prompts/bad name/SYSTEM.md": "x"}, "must match"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -188,7 +192,7 @@ func TestBuildBundleEmpty(t *testing.T) {
if err == nil {
t.Fatal("buildBundle(empty dir) = nil error, want rejection")
}
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, or agents/") {
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/") {
t.Errorf("buildBundle(empty) error %q does not name the no-members condition", err)
}
})
Expand All @@ -202,7 +206,7 @@ func TestBuildBundleEmpty(t *testing.T) {
if err == nil {
t.Fatal("buildBundle(only empty subdir) = nil error, want rejection")
}
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, or agents/") {
if !strings.Contains(err.Error(), "contains no members under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/") {
t.Errorf("buildBundle(empty subdir) error %q does not name the no-members condition", err)
}
})
Expand Down Expand Up @@ -234,16 +238,17 @@ func TestBundleCaps(t *testing.T) {
// between the builder's output and the door's grammar is caught here.
func TestBuildBundleDoorParity(t *testing.T) {
root := writeBundleDir(t, map[string]string{
"skills/alpha/SKILL.md": "# alpha",
"skills/alpha/ref/notes.md": "notes",
"extensions/beta/main.go": "package beta",
"mcp/gamma.json": `{"ok":true}`,
"settings/config.yml": "autoCompact: true\n",
"rules/delta.md": "# delta rule",
"rules/epsilon.mdc": "# epsilon rule",
"agents/zeta.md": "# zeta agent",
"AGENTS.md": "# fleet context",
"models.yml": "models:\n main: anthropic/claude\n",
"skills/alpha/SKILL.md": "# alpha",
"skills/alpha/ref/notes.md": "notes",
"extensions/beta/main.go": "package beta",
"mcp/gamma.json": `{"ok":true}`,
"settings/config.yml": "autoCompact: true\n",
"rules/delta.md": "# delta rule",
"rules/epsilon.mdc": "# epsilon rule",
"agents/zeta.md": "# zeta agent",
"AGENTS.md": "# fleet context",
"prompts/supervisor/SYSTEM.md": "# supervisor prompt",
"models.yml": "models:\n main: anthropic/claude\n",
})
bundle, err := buildBundle(root)
if err != nil {
Expand All @@ -269,6 +274,7 @@ func TestBuildBundleDoorParity(t *testing.T) {
"extensions/beta/main.go",
"mcp/gamma.json",
"models.yml",
"prompts/supervisor/SYSTEM.md",
"rules/delta.md",
"rules/epsilon.mdc",
"settings/config.yml",
Expand Down
3 changes: 2 additions & 1 deletion go/cmd/compass/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ func TestRenderConfigInfoAllBuckets(t *testing.T) {
McpServers: []string{"gamma"},
Rules: []string{"delta", "epsilon"},
Subagents: []string{"zeta"},
Prompts: []string{"supervisor"},
HasSettings: true,
HasAgentsMd: true,
HasModels: true,
Expand All @@ -393,7 +394,7 @@ func TestRenderConfigInfoAllBuckets(t *testing.T) {
if err := renderConfigInfo(&out, msg); err != nil {
t.Fatalf("renderConfigInfo: %v", err)
}
for _, want := range []string{"v9", "alpha", "beta", "gamma", "delta", "epsilon", "zeta"} {
for _, want := range []string{"v9", "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "supervisor"} {
if !strings.Contains(out.String(), want) {
t.Errorf("render output %q missing %q", out.String(), want)
}
Expand Down
47 changes: 33 additions & 14 deletions go/gen/compass/v1/compass.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion go/internal/linearagent/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ type OwnershipIndex interface {
// ManagerResolver walks a recorded authoring agent (possibly a peer) to its
// owning Manager and returns that Manager's account id and home channel id.
// It is its own narrow seam because no single store method spans the tree walk
// (up parent_agent_id to a role="manager" agent) AND the home-channel read; the
// (up parent_agent_id to the nearest tree ancestor — any Manager-class role,
// not a role=="manager" filter, since every tree node is now Manager-class and
// an owner parent must not be skipped) AND the home-channel read; the
// driver backs it with the store's agent-tree + account reads at assembly.
// store.ErrNotFound when the agent (or a walk ancestor) does not resolve.
type ManagerResolver interface {
Expand Down
18 changes: 17 additions & 1 deletion go/internal/runner/config_materialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const (
topDirSettings = "settings"
topDirRules = "rules"
topDirAgents = "agents"
topDirPrompts = "prompts"
)

// configTopDirs are the only permitted top-level directories in a config bundle.
Expand All @@ -75,6 +76,7 @@ var configTopDirs = map[string]struct{}{
topDirSettings: {},
topDirRules: {},
topDirAgents: {},
topDirPrompts: {},
}

// Top-level regular-file members admitted by exact filename (RIG-1678 T2), and
Expand All @@ -85,6 +87,9 @@ const (
memberAgentsMD = "AGENTS.md"
memberModels = "models.yml"
settingsMember = "settings/config.yml"
// memberSystemMD is the only filename admitted under prompts/<role>/
// (RIG-3075 T2); structural twin of the store door.
memberSystemMD = "SYSTEM.md"
)

// configFetcher is the T3 fetch seam the materializer pulls through. *ServerLink
Expand Down Expand Up @@ -481,7 +486,7 @@ func validateMemberPath(name string, typeflag byte) (string, error) {
return validateTopLevelMember(name, clean, top, typeflag)
}
if _, ok := configTopDirs[top]; !ok {
return "", fmt.Errorf("config bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, or agents/", name)
return "", fmt.Errorf("config bundle member %q is not under skills/, extensions/, mcp/, settings/, rules/, agents/, or prompts/", name)
}
return validateNestedMember(name, clean, top, parts, typeflag)
}
Expand Down Expand Up @@ -538,6 +543,17 @@ func validateNestedMember(name, clean, top string, parts []string, typeflag byte
return "", fmt.Errorf("config bundle mcp member name %q is not a safe name", entry)
}
return clean, nil
case topDirPrompts:
// Exactly prompts/<role>/SYSTEM.md — three components, safe <role>,
// filename exactly SYSTEM.md (RIG-3075 T2). Stricter than
// skills/extensions, so it needs its own arm, not the fall-through.
if len(parts) != 3 || parts[2] != memberSystemMD {
return "", fmt.Errorf("config bundle prompts member %q must be prompts/<role>/%s", name, memberSystemMD)
}
if !configTopLevelName.MatchString(parts[1]) {
return "", fmt.Errorf("config bundle prompts role name %q is not a safe name", parts[1])
}
return clean, nil
}
}

Expand Down
32 changes: 19 additions & 13 deletions go/internal/runner/config_materialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,12 +621,13 @@ var yamlMap = []byte("compaction:\n enabled: true\n")
func TestConfigMaterializeLandsNewMembers(t *testing.T) {
root := t.TempDir()
tarball := buildConfigTarball(t, map[string][]byte{
"settings/config.yml": yamlMap,
"AGENTS.md": []byte("# fleet conventions\n"),
"models.yml": []byte("providers:\n x:\n baseUrl: https://y\n"),
"rules/red-green.md": []byte("# red-green\n"),
"rules/hold-lane.mdc": []byte("# hold lane\n"),
"agents/design.md": []byte("# design agent\n"),
"settings/config.yml": yamlMap,
"AGENTS.md": []byte("# fleet conventions\n"),
"models.yml": []byte("providers:\n x:\n baseUrl: https://y\n"),
"rules/red-green.md": []byte("# red-green\n"),
"rules/hold-lane.mdc": []byte("# hold lane\n"),
"agents/design.md": []byte("# design agent\n"),
"prompts/supervisor/SYSTEM.md": []byte("# supervisor prompt\n"),
})
f := &fakeConfigFetcher{bundle: AgentConfigBundle{Version: "v1", Tarball: tarball}}
m := NewConfigMaterializer(root, f, nil)
Expand All @@ -636,12 +637,13 @@ func TestConfigMaterializeLandsNewMembers(t *testing.T) {
versionDir := filepath.Join(root, "v1")

files := map[string]string{
filepath.Join("settings", "config.yml"): string(yamlMap),
"AGENTS.md": "# fleet conventions\n",
"models.yml": "providers:\n x:\n baseUrl: https://y\n",
filepath.Join("rules", "red-green.md"): "# red-green\n",
filepath.Join("rules", "hold-lane.mdc"): "# hold lane\n",
filepath.Join("agents", "design.md"): "# design agent\n",
filepath.Join("settings", "config.yml"): string(yamlMap),
"AGENTS.md": "# fleet conventions\n",
"models.yml": "providers:\n x:\n baseUrl: https://y\n",
filepath.Join("rules", "red-green.md"): "# red-green\n",
filepath.Join("rules", "hold-lane.mdc"): "# hold lane\n",
filepath.Join("agents", "design.md"): "# design agent\n",
filepath.Join("prompts", "supervisor", "SYSTEM.md"): "# supervisor prompt\n",
}
for rel, want := range files {
p := filepath.Join(versionDir, rel)
Expand All @@ -654,7 +656,7 @@ func TestConfigMaterializeLandsNewMembers(t *testing.T) {
t.Fatalf("%s mode = %v err=%v, want 0644", rel, fi.Mode().Perm(), err)
}
}
for _, dir := range []string{"settings", "rules", "agents"} {
for _, dir := range []string{"settings", "rules", "agents", "prompts"} {
di, err := os.Stat(filepath.Join(versionDir, dir))
if err != nil || di.Mode().Perm() != 0o755 {
t.Fatalf("%s dir mode = %v err=%v, want 0755", dir, di.Mode().Perm(), err)
Expand All @@ -679,6 +681,10 @@ func TestConfigMaterializeRejectsNewMemberStructure(t *testing.T) {
"agents nested": buildConfigTarball(t, map[string][]byte{"agents/sub/a.md": []byte("x")}),
"top-level other file": buildConfigTarball(t, map[string][]byte{"README.md": []byte("x")}),
"models non-mapping": buildConfigTarball(t, map[string][]byte{"models.yml": []byte("- a\n")}),
"prompts wrong filename": buildConfigTarball(t, map[string][]byte{"prompts/supervisor/other.md": []byte("x")}),
"prompts too deep": buildConfigTarball(t, map[string][]byte{"prompts/supervisor/sub/SYSTEM.md": []byte("x")}),
"prompts too shallow": buildConfigTarball(t, map[string][]byte{"prompts/SYSTEM.md": []byte("x")}),
"prompts bad role name": buildConfigTarball(t, map[string][]byte{"prompts/bad name/SYSTEM.md": []byte("x")}),
}
for name, tarball := range cases {
t.Run(name, func(t *testing.T) {
Expand Down
Loading
Loading