feat(veans): add prime command for agent prompt injection
This commit is contained in:
parent
b9551d55ba
commit
e8cdfcf023
|
|
@ -0,0 +1,87 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.vikunja.io/veans/internal/client"
|
||||
"code.vikunja.io/veans/internal/config"
|
||||
)
|
||||
|
||||
//go:embed prompt.tmpl
|
||||
var promptTemplate string
|
||||
|
||||
// primeContext is the data passed into the agent prompt template.
|
||||
type primeContext struct {
|
||||
Server string
|
||||
ProjectID int64
|
||||
ProjectTitle string
|
||||
ProjectIdentifier string
|
||||
ViewID int64
|
||||
Buckets config.Buckets
|
||||
BotUsername string
|
||||
TaskIDExample string
|
||||
}
|
||||
|
||||
func newPrimeCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "prime",
|
||||
Short: "Emit the agent system prompt for this project",
|
||||
Long: `Renders the embedded prompt template against this repo's .veans.yml and
|
||||
prints it to stdout. Designed to be wired into Claude Code's SessionStart
|
||||
and PreCompact hooks (or the OpenCode equivalent) so coding agents always
|
||||
have an up-to-date Vikunja cheat sheet in context.
|
||||
|
||||
If no .veans.yml is found upward from the current directory, prime exits
|
||||
silently with status 0 — that makes the hook safe to install globally.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path, err := config.Find("")
|
||||
if err != nil {
|
||||
if errors.Is(err, config.ErrNotFound) {
|
||||
return nil // silent — globally-installed hook safety
|
||||
}
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch the project title for nicer prompt copy. Best-effort —
|
||||
// if the API call fails (network blip, expired token), we fall
|
||||
// back to "(unknown)" rather than aborting the prompt render.
|
||||
projectTitle := "(unknown)"
|
||||
if rt, err := loadRuntime(); err == nil {
|
||||
if p, err := rt.client.GetProject(cmd.Context(), cfg.ProjectID); err == nil {
|
||||
projectTitle = p.Title
|
||||
}
|
||||
}
|
||||
|
||||
data := primeContext{
|
||||
Server: cfg.Server,
|
||||
ProjectID: cfg.ProjectID,
|
||||
ProjectTitle: projectTitle,
|
||||
ProjectIdentifier: cfg.ProjectIdentifier,
|
||||
ViewID: cfg.ViewID,
|
||||
Buckets: cfg.Buckets,
|
||||
BotUsername: cfg.Bot.Username,
|
||||
TaskIDExample: cfg.FormatTaskID(1),
|
||||
}
|
||||
|
||||
tpl, err := template.New("prime").Parse(promptTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse prompt template: %w", err)
|
||||
}
|
||||
return tpl.Execute(cmd.OutOrStdout(), data)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// silence linter noise on unused symbols when wiring hooks.
|
||||
var _ = client.New
|
||||
var _ = strings.TrimSpace
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"code.vikunja.io/veans/internal/config"
|
||||
)
|
||||
|
||||
func TestPrimeTemplate_RendersAnchors(t *testing.T) {
|
||||
data := primeContext{
|
||||
Server: "https://vikunja.example.com",
|
||||
ProjectID: 42,
|
||||
ProjectTitle: "Test Project",
|
||||
ProjectIdentifier: "PROJ",
|
||||
ViewID: 7,
|
||||
Buckets: config.Buckets{Todo: 11, InProgress: 12, InReview: 13, Done: 14, Scrapped: 15},
|
||||
BotUsername: "bot-myrepo",
|
||||
TaskIDExample: "PROJ-1",
|
||||
}
|
||||
tpl, err := template.New("prime").Parse(promptTemplate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := buf.String()
|
||||
|
||||
mustContain := []string{
|
||||
"<EXTREMELY_IMPORTANT>",
|
||||
"</EXTREMELY_IMPORTANT>",
|
||||
"bot-myrepo",
|
||||
"Test Project",
|
||||
"PROJ-1",
|
||||
"Refs:",
|
||||
"veans claim",
|
||||
"veans list --ready",
|
||||
"--description-replace-old",
|
||||
"Todo",
|
||||
"In Progress",
|
||||
"In Review",
|
||||
"Done",
|
||||
"Scrapped",
|
||||
}
|
||||
for _, s := range mustContain {
|
||||
if !strings.Contains(out, s) {
|
||||
t.Errorf("rendered prompt missing %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Buckets show concrete IDs.
|
||||
for _, want := range []string{"`11`", "`12`", "`13`", "`14`", "`15`"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("bucket id %s not present in output", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimeTemplate_NoIdentifierFallback(t *testing.T) {
|
||||
data := primeContext{
|
||||
ProjectTitle: "No Ident",
|
||||
ProjectIdentifier: "",
|
||||
BotUsername: "bot-x",
|
||||
TaskIDExample: "#1",
|
||||
Server: "https://vikunja.example.com",
|
||||
ProjectID: 1,
|
||||
ViewID: 1,
|
||||
}
|
||||
tpl, _ := template.New("prime").Parse(promptTemplate)
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "no identifier") {
|
||||
t.Errorf("expected fallback copy when project has no identifier; got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "#NN") {
|
||||
t.Errorf("expected #NN format mention in fallback")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<EXTREMELY_IMPORTANT>
|
||||
You are working in a repository configured to track tasks in Vikunja via the
|
||||
`veans` CLI. **You MUST use veans for all task tracking instead of TodoWrite.**
|
||||
|
||||
Project: {{ .ProjectTitle }}{{ if .ProjectIdentifier }} ({{ .ProjectIdentifier }}){{ end }}
|
||||
Bot identity: `{{ .BotUsername }}` — your actions in Vikunja appear as this user.
|
||||
Server: {{ .Server }}
|
||||
</EXTREMELY_IMPORTANT>
|
||||
|
||||
# Workflow
|
||||
|
||||
## BEFORE you start work
|
||||
- If a task already exists, claim it: `veans claim {{ .TaskIDExample }}`
|
||||
- Otherwise, create one and start it in one step:
|
||||
`veans create "<short title>" -s in-progress -d "<short description>"`
|
||||
- Use `veans list --ready` to find tasks ready to start (Todo + not blocked).
|
||||
|
||||
## WHILE you work
|
||||
- Keep the task's description in sync with what you're doing. Use markdown
|
||||
checkboxes for the small steps you're ticking off:
|
||||
`veans update {{ .TaskIDExample }} --description-append "- [ ] step 1"`
|
||||
- For surgical edits to the description, prefer:
|
||||
`veans update {{ .TaskIDExample }} --description-replace-old "- [ ] step 1" --description-replace-new "- [x] step 1"`
|
||||
(errors if the old text isn't unique — mirrors the Edit tool semantics)
|
||||
- Post a comment on significant decisions, discoveries, or course-changes:
|
||||
`veans update {{ .TaskIDExample }} --comment "Discovered Y; pivoting to Z because …"`
|
||||
- For sub-work that could be assigned separately, create real subtasks
|
||||
via `--parent`. For incremental check-off lists, use markdown checkboxes
|
||||
in the description instead.
|
||||
|
||||
## AFTER you finish work
|
||||
- Move to `in-review` and post a summary comment. **Never close tasks
|
||||
yourself** — the human (or the merge hook) closes them.
|
||||
```
|
||||
veans update {{ .TaskIDExample }} -s in-review --comment "## Summary of Changes
|
||||
- …
|
||||
- …"
|
||||
```
|
||||
- If you abandon work, scrap the task with a reason:
|
||||
`veans update {{ .TaskIDExample }} -s scrapped --reason "obsolete: <why>"`
|
||||
|
||||
## Commit messages
|
||||
Include the task identifier on a `Refs:` line so the merge hook can close
|
||||
tasks automatically when the PR lands:
|
||||
|
||||
```
|
||||
fix: handle empty project identifiers
|
||||
|
||||
Refs: {{ .TaskIDExample }}
|
||||
```
|
||||
|
||||
# Status model
|
||||
|
||||
| Status | Bucket name | Done flag | Who moves there? |
|
||||
| ------------- | -------------- | --------- | ---------------------------------------- |
|
||||
| `todo` | Todo | false | created here by default |
|
||||
| `in-progress` | In Progress | false | `veans claim` or `update -s in-progress` |
|
||||
| `in-review` | In Review | false | you, when work is finished |
|
||||
| `completed` | Done | true | humans / merge hook only |
|
||||
| `scrapped` | Scrapped | true | you, with --reason |
|
||||
|
||||
# Common commands
|
||||
|
||||
```
|
||||
veans list # all tasks, tree view
|
||||
veans list --ready # ready to start (Todo + not blocked)
|
||||
veans list --mine # tasks assigned to you
|
||||
veans list --branch # tasks tagged with the current git branch
|
||||
veans list --filter "priority > 3" # raw Vikunja filter expression
|
||||
veans show {{ .TaskIDExample }} # full task detail
|
||||
veans show {{ .TaskIDExample }} --json # JSON for parsing
|
||||
|
||||
veans create "title" -s in-progress -d "description"
|
||||
veans create "title" --label bug --priority 4 --parent {{ .TaskIDExample }}
|
||||
veans create "title" --blocked-by {{ .TaskIDExample }}
|
||||
|
||||
veans update {{ .TaskIDExample }} -s in-review --comment "..."
|
||||
veans update {{ .TaskIDExample }} --description-append "- [ ] new step"
|
||||
veans update {{ .TaskIDExample }} --description-replace-old "- [ ] x" --description-replace-new "- [x] x"
|
||||
veans update {{ .TaskIDExample }} --label-add bug --label-remove flaky
|
||||
veans update {{ .TaskIDExample }} -s scrapped --reason "obsolete: replaced by {{ .TaskIDExample }}"
|
||||
|
||||
veans claim {{ .TaskIDExample }} # assign yourself + In Progress + branch label
|
||||
veans api GET /tasks/... # escape hatch for raw REST when curated cmds don't fit
|
||||
```
|
||||
|
||||
# Project quick reference
|
||||
|
||||
- Project ID: `{{ .ProjectID }}`{{ if .ProjectIdentifier }}, identifier: `{{ .ProjectIdentifier }}` (task IDs render as `{{ .ProjectIdentifier }}-NN`){{ else }} (no identifier; task IDs render as `#NN`){{ end }}
|
||||
- Kanban view: `{{ .ViewID }}`
|
||||
- Buckets: Todo=`{{ .Buckets.Todo }}`, In Progress=`{{ .Buckets.InProgress }}`, In Review=`{{ .Buckets.InReview }}`, Done=`{{ .Buckets.Done }}`, Scrapped=`{{ .Buckets.Scrapped }}`
|
||||
- Label namespace: `veans:` (auto-prepended; e.g. `--label bug` becomes `veans:bug`)
|
||||
- Branch labels: `veans:branch:<branch-name>` — `veans claim` adds the current branch automatically
|
||||
|
||||
The agent prompt above overrides TodoWrite. Anything you would have tracked
|
||||
there should live in Vikunja instead, where it stays visible across sessions
|
||||
and to humans collaborating on the project.
|
||||
|
|
@ -40,6 +40,7 @@ func Root(version string) *cobra.Command {
|
|||
root.AddCommand(newCreateCmd())
|
||||
root.AddCommand(newUpdateCmd())
|
||||
root.AddCommand(newClaimCmd())
|
||||
root.AddCommand(newPrimeCmd())
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue