Diff
1diff --git a/cmd/jp/main.go b/cmd/jp/main.go
2index f4280f829ac62bc6c814b5c3742b343ee2ff3b58..79166aef5fa4dba67eff0717fff29fbf5dd4ae79 100644
3--- a/cmd/jp/main.go
4+++ b/cmd/jp/main.go
5@@ -2,8 +2,6 @@
6 package main
7
8 import (
9- "context"
10- "encoding/json"
11 "fmt"
12 "io"
13 "net/http"
14@@ -32,8 +30,6 @@ func main() {
15 switch os.Args[1] {
16 case "run":
17 runCmd(os.Args[2:])
18- case "persona":
19- personaCmd(os.Args[2:])
20 case "help", "-h", "--help":
21 usage(os.Stdout)
22 default:
23@@ -149,51 +145,13 @@ func fatalf(format string, args ...any) {
24 os.Exit(1)
25 }
26
27-func personaCmd(args []string) {
28- if len(args) == 0 || args[0] != "gen" {
29- fmt.Fprintln(os.Stderr, "jp persona: expected subcommand 'gen'")
30- os.Exit(1)
31- }
32- genPersonaCmd(args[1:])
33-}
34-
35-type genOptions struct {
36- config.Config
37- Role string `long:"role" required:"true" description:"role to author a persona for"`
38-}
39-
40-func genPersonaCmd(args []string) {
41- opts := parseOpts[genOptions]("persona gen", args)
42-
43- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
44- defer cancel()
45- client := llm.NewClient(opts.LLMBaseURL, nil)
46- draft, err := persona.GenerateDraft(ctx, client, opts.Role)
47- if err != nil {
48- fmt.Fprintf(os.Stderr, "persona gen: LLM service at %s unavailable: %v\n", opts.LLMBaseURL, err)
49- os.Exit(1)
50- }
51-
52- out, merr := json.MarshalIndent(draft, "", " ")
53- if merr != nil {
54- fmt.Fprintf(os.Stderr, "persona gen: encode draft: %v\n", merr)
55- os.Exit(1)
56- }
57- if _, werr := fmt.Fprintln(os.Stdout, string(out)); werr != nil {
58- os.Exit(1)
59- }
60- fmt.Fprintln(os.Stderr, "DRAFT persona. Requires human review before use.")
61- fmt.Fprintln(os.Stderr, "Nothing was written to assets/personas/. Review it and save it yourself.")
62-}
63-
64 func usage(w io.Writer) {
65 _, _ = fmt.Fprint(w, `jp - Japanese learning RPG
66
67 Usage:
68- jp run [flags] Run the game
69- jp persona gen --role R Author a persona draft
70- jp help Show this help
71+ jp run [flags] Run the game
72+ jp help Show this help
73
74-Run "jp run -help" or "jp persona gen -help" for flag details.
75+Run "jp run -help" for flag details.
76 `)
77 }
78diff --git a/internal/llm/contract.go b/internal/llm/contract.go
79index ffb93543c4f8445aea93c09907b8c96f43fe7c52..15c8dd648885abc5a1a7ef45dabe717bb0323dbb 100644
80--- a/internal/llm/contract.go
81+++ b/internal/llm/contract.go
82@@ -92,13 +92,6 @@ func requireFields(vals map[string]string, names []string) error {
83 return nil
84 }
85
86-// ParseFields is the shared strict FIELD|value parser for model output
87-// contracts. It tolerates surrounding whitespace and returns a recoverable
88-// *ContractError on malformed lines, unknown fields, or duplicate fields.
89-func ParseFields(raw string, names ...string) (map[string]string, error) {
90- return parseFields(raw, names)
91-}
92-
93 type NPCReply struct {
94 Romaji string
95 Kana string
96diff --git a/internal/persona/draft.go b/internal/persona/draft.go
97deleted file mode 100644
98index fc9783a52b2189da3bf7967584024c878b8d3295..0000000000000000000000000000000000000000
99--- a/internal/persona/draft.go
100+++ /dev/null
101@@ -1,71 +0,0 @@
102-package persona
103-
104-import (
105- "context"
106-
107- "japanese/internal/llm"
108-)
109-
110-// PersonaDraft is an LLM-authored, not-yet-reviewed persona. It carries only
111-// the fields the model can author; location, situation, and ambiguity are
112-// filled in by a human during review before it becomes a stored Persona.
113-type PersonaDraft struct {
114- Name string `json:"name"`
115- Personality string `json:"personality"`
116- SpeechStyle string `json:"speech_style"`
117-}
118-
119-const draftSystem = `You write one person's profile for use in a realistic everyday conversation.
120-Return exactly three lines and nothing else, in this order:
121-NAME|<a natural name for the person>
122-PERSONALITY|<two to four personality traits separated by commas>
123-SPEECH_STYLE|<one sentence describing how they speak>`
124-
125-// DraftSystemPrompt returns the game-free system prompt for persona authoring.
126-// It names no game, player, NPC, quest, or scenario.
127-func DraftSystemPrompt() string { return draftSystem }
128-
129-// DraftUserPrompt returns the user prompt that supplies the role to author.
130-func DraftUserPrompt(role string) string {
131- return "Write the profile of a real person whose role is: " + role + ". They are in an ordinary, real-world situation."
132-}
133-
134-var draftFieldNames = []string{"NAME", "PERSONALITY", "SPEECH_STYLE"}
135-
136-// GenerateDraft asks the configured LLM client for a persona draft for the
137-// given role and parses the strict three-field result. It issues exactly one
138-// chat request; it never starts, probes, or manages any service.
139-func GenerateDraft(ctx context.Context, client *llm.Client, role string) (PersonaDraft, error) {
140- msgs := []llm.Message{
141- {Role: llm.RoleSystem, Content: DraftSystemPrompt()},
142- {Role: llm.RoleUser, Content: DraftUserPrompt(role)},
143- }
144- raw, err := client.Generate(ctx, msgs, llm.Qwen3Options())
145- if err != nil {
146- return PersonaDraft{}, err
147- }
148- return ParseDraft(raw)
149-}
150-
151-// ParseDraft parses the strict three-field persona draft contract. Missing or
152-// empty fields and malformed lines are recoverable *llm.ContractError values.
153-func ParseDraft(raw string) (PersonaDraft, error) {
154- vals, err := llm.ParseFields(raw, draftFieldNames...)
155- if err != nil {
156- return PersonaDraft{}, err
157- }
158- for _, name := range draftFieldNames {
159- v, ok := vals[name]
160- if !ok {
161- return PersonaDraft{}, &llm.ContractError{Kind: llm.MissingField, Field: name}
162- }
163- if v == "" {
164- return PersonaDraft{}, &llm.ContractError{Kind: llm.EmptyValue, Field: name}
165- }
166- }
167- return PersonaDraft{
168- Name: vals["NAME"],
169- Personality: vals["PERSONALITY"],
170- SpeechStyle: vals["SPEECH_STYLE"],
171- }, nil
172-}