diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 7a25e8b..1c3d8c9 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -57,6 +57,8 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("agentic:review-queue", core.Command{Description: "Process the CodeRabbit review queue", Action: s.cmdReviewQueue}) c.Command("status", core.Command{Description: "List agent workspace statuses", Action: s.cmdStatus}) c.Command("prompt", core.Command{Description: "Build and display an agent prompt for a repo", Action: s.cmdPrompt}) + c.Command("prompt/version", core.Command{Description: "Read the current prompt snapshot for a workspace", Action: s.cmdPromptVersion}) + c.Command("agentic:prompt/version", core.Command{Description: "Read the current prompt snapshot for a workspace", Action: s.cmdPromptVersion}) c.Command("extract", core.Command{Description: "Extract a workspace template to a directory", Action: s.cmdExtract}) s.registerPlanCommands() s.registerSessionCommands() @@ -663,6 +665,38 @@ func (s *PrepSubsystem) cmdPrompt(options core.Options) core.Result { return core.Result{OK: true} } +func (s *PrepSubsystem) cmdPromptVersion(options core.Options) core.Result { + workspace := optionStringValue(options, "workspace", "_arg") + if workspace == "" { + core.Print(nil, "usage: core-agent prompt version ") + return core.Result{Value: core.E("agentic.cmdPromptVersion", "workspace is required", nil), OK: false} + } + + result := s.handlePromptVersion(s.commandContext(), core.NewOptions( + core.Option{Key: "workspace", Value: workspace}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPromptVersion", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(PromptVersionOutput) + if !ok { + err := core.E("agentic.cmdPromptVersion", "invalid prompt version output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "workspace: %s", output.Workspace) + core.Print(nil, "hash: %s", output.Snapshot.Hash) + if output.Snapshot.CreatedAt != "" { + core.Print(nil, "created: %s", output.Snapshot.CreatedAt) + } + core.Print(nil, "chars: %d", len(output.Snapshot.Content)) + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) cmdExtract(options core.Options) core.Result { templateName := options.String("_arg") if templateName == "" { diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 7d5817d..ee99aba 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1376,6 +1376,7 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "brain:forget") assert.Contains(t, cmds, "status") assert.Contains(t, cmds, "prompt") + assert.Contains(t, cmds, "prompt/version") assert.Contains(t, cmds, "extract") assert.Contains(t, cmds, "lang/detect") assert.Contains(t, cmds, "lang/list") diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 87d936d..34bf719 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -261,6 +261,7 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { c.Action("content.schema.generate", s.handleContentSchemaGenerate).Description = "Generate SEO schema JSON-LD for article, FAQ, or how-to content" c.Action("agentic.prompt", s.handlePrompt).Description = "Read a system prompt by slug" + c.Action("agentic.prompt.version", s.handlePromptVersion).Description = "Read the current prompt snapshot for a workspace" c.Action("agentic.task", s.handleTask).Description = "Read a task plan by slug" c.Action("agentic.flow", s.handleFlow).Description = "Read a build/release flow by slug" c.Action("agentic.persona", s.handlePersona).Description = "Read a persona by path" @@ -809,6 +810,30 @@ func writePromptSnapshot(workspaceDir, prompt string) core.Result { return core.Result{Value: hash, OK: true} } +// snapshot := readPromptSnapshot("/srv/.core/workspace/core/go-io/task-42") +func readPromptSnapshot(workspaceDir string) (PromptVersionSnapshot, error) { + if workspaceDir == "" { + return PromptVersionSnapshot{}, core.E("readPromptSnapshot", "workspace is required", nil) + } + + snapshotPath := core.JoinPath(WorkspaceMetaDir(workspaceDir), "prompt-version.json") + result := fs.Read(snapshotPath) + if !result.OK { + err, _ := result.Value.(error) + if err == nil { + err = core.E("readPromptSnapshot", "prompt snapshot not found", nil) + } + return PromptVersionSnapshot{}, err + } + + var snapshot PromptVersionSnapshot + if parseResult := core.JSONUnmarshalString(result.Value.(string), &snapshot); !parseResult.OK { + err, _ := parseResult.Value.(error) + return PromptVersionSnapshot{}, core.E("readPromptSnapshot", "failed to parse prompt snapshot", err) + } + return snapshot, nil +} + // snapshot := PromptVersionSnapshot{Hash: "f2c8...", Content: "TASK: Fix tests"} type PromptVersionSnapshot struct { Hash string `json:"hash"` diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 7a14a8b..69eb545 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -688,6 +688,9 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "session/replay") assert.Contains(t, c.Commands(), "review-queue") assert.Contains(t, c.Commands(), "agentic:review-queue") + assert.Contains(t, c.Commands(), "prompt/version") + assert.Contains(t, c.Commands(), "agentic:prompt/version") + assert.True(t, c.Action("agentic.prompt.version").Exists()) assert.Contains(t, c.Commands(), "task") assert.Contains(t, c.Commands(), "task/create") assert.Contains(t, c.Commands(), "task/update") diff --git a/pkg/agentic/prompt_version.go b/pkg/agentic/prompt_version.go new file mode 100644 index 0000000..39b39b3 --- /dev/null +++ b/pkg/agentic/prompt_version.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + + core "dappco.re/go/core" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// version := agentic.PromptVersionOutput{Success: true, Snapshot: agentic.PromptVersionSnapshot{Hash: "..." }} +type PromptVersionOutput struct { + Success bool `json:"success"` + Workspace string `json:"workspace"` + Snapshot PromptVersionSnapshot `json:"snapshot"` +} + +// result := c.Action("agentic.prompt.version").Run(ctx, core.NewOptions( +// +// core.Option{Key: "workspace", Value: "/srv/.core/workspace/core/go-io/task-42"}, +// +// )) +func (s *PrepSubsystem) handlePromptVersion(ctx context.Context, options core.Options) core.Result { + workspace := optionStringValue(options, "workspace", "_arg") + _, output, err := s.promptVersion(ctx, nil, workspace) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) promptVersion(_ context.Context, _ *mcp.CallToolRequest, workspace string) (*mcp.CallToolResult, PromptVersionOutput, error) { + workspaceDir := s.resolveWorkspaceDir(workspace) + if workspaceDir == "" { + return nil, PromptVersionOutput{}, core.E("promptVersion", "workspace is required", nil) + } + + snapshot, err := readPromptSnapshot(workspaceDir) + if err != nil { + return nil, PromptVersionOutput{}, err + } + + return nil, PromptVersionOutput{ + Success: true, + Workspace: workspace, + Snapshot: snapshot, + }, nil +} diff --git a/pkg/agentic/prompt_version_test.go b/pkg/agentic/prompt_version_test.go new file mode 100644 index 0000000..5dad545 --- /dev/null +++ b/pkg/agentic/prompt_version_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "testing" + + core "dappco.re/go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPromptVersion_ReadPromptSnapshot_Good(t *testing.T) { + workspaceDir := t.TempDir() + prompt := "TASK: Fix tests\n\nRead CODEX.md and commit when done." + + writeResult := writePromptSnapshot(workspaceDir, prompt) + require.True(t, writeResult.OK) + + snapshot, err := readPromptSnapshot(workspaceDir) + require.NoError(t, err) + + assert.Equal(t, promptSnapshotHash(prompt), snapshot.Hash) + assert.Contains(t, snapshot.CreatedAt, "T") + assert.Equal(t, prompt, snapshot.Content) +} + +func TestPromptVersion_ReadPromptSnapshot_Bad_MissingWorkspace(t *testing.T) { + _, err := readPromptSnapshot("") + require.Error(t, err) + assert.Contains(t, err.Error(), "workspace is required") +} + +func TestPromptVersion_ReadPromptSnapshot_Ugly_InvalidJson(t *testing.T) { + workspaceDir := t.TempDir() + metaDir := WorkspaceMetaDir(workspaceDir) + require.True(t, fs.EnsureDir(metaDir).OK) + require.True(t, fs.Write(core.JoinPath(metaDir, "prompt-version.json"), "{not-json").OK) + + _, err := readPromptSnapshot(workspaceDir) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse prompt snapshot") +}