docs(plugins): add native prompt author example

This commit is contained in:
xiexian committed 2026-10-02 17:09:22 +08:00
1 parent b66b2c34ff
commit 4e76141a41
6 files changed
+354

No files matched your search

+4
View File
@@ -384,6 +384,10 @@ New resource types:
with the plugin;
- any other file listed under `themes` is ignored.
The [native prompt example](../examples/work-note-kit/README.md) shows a
template-only package, nested names, argument substitution, and the
install/update/disable/remove lifecycle without a compiler or sidecar.
The `runtime` block declares a code extension — a sidecar process Reasonix
launches and talks to over the Extension Protocol (JSON-RPC 2.0 over stdio;
see `docs/EXTENSION_PROTOCOL.generated.md` for the method index and
+100
View File
@@ -0,0 +1,100 @@
---
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-02
---
# Native prompt template example
1. This native v2 package contributes two Markdown prompt templates. It has
no runtime, hook, MCP server, compiler, or installation script.
2. Templates become ordinary user turns when explicitly invoked. They supply
model guidance; they do not run checks, limit tools, or grant permission.
3. Use a normally configured model for the manual exercise. Its normal account
access and usage charges apply. Model output quality needs manual evaluation.
## Install and discover
From the Reasonix repository root, preview and install a copy:
```sh
reasonix plugin install ./examples/work-note-kit --dry-run
reasonix plugin install ./examples/work-note-kit --yes
reasonix plugin doctor work-note-kit
reasonix plugin show work-note-kit
```
1. Preview must leave the package uninstalled. The installed inventory must
report two prompts and no runtime or executable contribution.
2. `contributes.prompts` names the `prompts` directory, not an individual file.
A template name comes from its path: `handoff.md` becomes `handoff`, and
`checks/summarize.md` becomes `checks:summarize`.
3. Start a new session after installation, or use `/reload` while idle. In
Studio, use **Reload runtime** in **Settings → Extension → Installed**.
The package prefix makes invocations distinct from other authors' templates.
## Run the exercise
Enter these in an interactive session:
```text
/work-note-kit:handoff lint-passed typecheck-passed integration-not-run
/work-note-kit:checks:summarize build unit-passed network-not-run
```
1. The handoff should preserve both passing checks, identify integration as
unverified, and avoid claiming deployment or an independent review.
2. The check summary should use `build` as its label and preserve the full
observations, including `network-not-run`. `$1` is the first whitespace
separated argument; `$ARGUMENTS` includes all arguments, including the label.
3. `$$` emits one literal dollar sign. Templates are text substitution, not
shell commands: quotes do not group arguments and no expression is evaluated.
4. Run `/work-note-kit:handoff` without arguments as a failure exercise. The
template still reaches the model; a useful answer asks for missing facts.
This is model behavior to assess, not a host-side input-validation guarantee.
5. Use qualified names. A short-name compatibility alias may be absent when a
project command or another package owns that name.
## Edit, replace and remove
1. Edit a source template. A copy installation keeps its own files; source
edits do not change that installed copy. Preview and apply replacement,
then start a new session or reload while idle:
```sh
reasonix plugin install ./examples/work-note-kit --replace --dry-run
reasonix plugin install ./examples/work-note-kit --replace --yes
reasonix plugin disable work-note-kit
reasonix plugin enable work-note-kit
reasonix plugin remove work-note-kit --yes
```
2. Disable preserves the installed copy but excludes its templates from newly
built sessions. Enable restores discovery. Removal deletes the managed copy
and registration while keeping the original source.
3. For link-based authoring, remove the copy, preview using `--link --dry-run`,
then install using `--link --yes`. Keep the linked source available and reload
after edits. Removing a link preserves its external source directory.
4. Disable and removal do not erase a template already sent in session history.
Use a fresh session when checking that a contribution is absent.
## Verify the host path
```sh
go test ./internal/assembly/boot/ -run '^TestEffectWorkNotePromptExampleLifecycle$' -count=1
```
1. The effect test uses the real approved-plan installer and `boot.Build`. It
checks copied templates, source-edit isolation, replacement preview/application,
disable/enable/removal, qualified naming beside a project-owned short command,
and rendered arguments in actual provider user messages.
2. The provider is scripted. This proves host routing and substitution, not a
live model's handoff quality or a public market publication.
| Observation | Check |
| --- | --- |
| No template in a fresh session | Check package enablement and the declared prompt directory; run `plugin show`. |
| The short name runs a different command | Use the full `/work-note-kit:<name>` invocation. |
| A source edit has no effect | Replace the installed copy, or use a link, then reload or build a fresh session. |
| The answer invents a check result | Compare it with the supplied observations; prompt guidance does not enforce truthfulness. |
@@ -0,0 +1,21 @@
---
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-02
description: Summarize supplied check evidence and its limits
argument-hint: <label> <observations...>
---
Summarize the supplied check evidence, keeping observation separate from inference.
Label: $1
Observations: $ARGUMENTS
A literal dollar sign is $$.
1. Name each reported check, its scope and observed result. Preserve failures
and checks not run; do not infer success from the absence of a failure.
2. If the label or observations are missing, identify the missing input. Do
not fabricate logs, test counts, platforms, or a reviewer verdict.
3. Explain what the evidence does not establish. Use the caller's language.
This prompt does not execute a check or enforce an execution boundary.
+22
View File
@@ -0,0 +1,22 @@
---
owner: "@esengine"
backup: "@SivanCola"
status: active
reviewed: 2026-10-02
description: Draft a handoff from supplied task facts without inventing results
argument-hint: <task facts...>
---
Draft a short work handoff using only the facts supplied by the caller.
Facts supplied by the caller:
$ARGUMENTS
1. Report the outcome, evidence of checks actually performed, and remaining
work in separate sections. Preserve identifiers and qualifications.
2. Treat a check marked not run as unverified. A passing unit test does not
establish an integration result, deployment, or production readiness.
3. If the input is empty or essential facts are absent, name the missing facts
and ask for them instead of claiming completion.
4. Keep the answer in the caller's requested language. This template supplies
guidance; it grants no permission to edit files, run checks, or contact others.
@@ -0,0 +1,9 @@
{
"apiVersion": "reasonix.io/plugin/v2",
"name": "work-note-kit",
"version": "1.0.0",
"description": "Prompt templates for an evidence-based handoff and check summary",
"contributes": {
"prompts": ["prompts"]
}
}
@@ -0,0 +1,198 @@
package boot
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"reasonix/internal/contract/event"
"reasonix/internal/contract/provider"
"reasonix/internal/ext/installsource"
"reasonix/internal/ext/pluginpkg"
"reasonix/internal/runtime/agent/testutil"
)
func TestEffectWorkNotePromptExampleLifecycle(t *testing.T) {
registerBootTokenProfileTestProvider()
example, err := filepath.Abs(filepath.Join("..", "..", "..", "examples", "work-note-kit"))
if err != nil {
t.Fatal(err)
}
source := robustTempDir(t)
if err := os.CopyFS(source, os.DirFS(example)); err != nil {
t.Fatal(err)
}
home := isolateConfigHome(t)
reasonixHome := filepath.Join(home, ".reasonix")
t.Setenv("REASONIX_HOME", reasonixHome)
workspace := robustTempDir(t)
t.Chdir(workspace)
writeFile(t, workspace, ".reasonix/commands/handoff.md", "PROJECT HANDOFF $ARGUMENTS")
writeFile(t, workspace, "reasonix.toml", `
default_model = "test-model"
[agent]
system_prompt = "PROMPT EXAMPLE BASE"
[environment]
enabled = false
[codegraph]
enabled = false
[[providers]]
name = "test-model"
kind = "boot-token-profile-test"
model = "x"
`)
approveWorkspace(t, workspace)
installer := installsource.NewTool(installsource.Options{ProjectRoot: workspace, HomeDir: home, RequireApprovedPlan: true})
type result struct {
OK bool `json:"ok"`
Applied bool `json:"applied"`
Status string `json:"status"`
PlanID string `json:"planId"`
}
install := func(args map[string]any) result {
t.Helper()
raw, err := json.Marshal(args)
if err != nil {
t.Fatal(err)
}
out, err := installer.Execute(t.Context(), raw)
if err != nil {
t.Fatal(err)
}
var r result
if err := json.Unmarshal([]byte(out), &r); err != nil || !r.OK {
t.Fatalf("install_source=%s, err=%v", out, err)
}
return r
}
check := func(t *testing.T, present, updated bool) {
t.Helper()
rec := testutil.NewMock("work-note-example", testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"}, testutil.Turn{Text: "ok"})
setBootTokenProfileTestProvider(t, rec)
ctrl, err := Build(t.Context(), Options{Sink: event.Discard, SessionDir: filepath.Join(robustTempDir(t), "sessions")})
if err != nil {
t.Fatal(err)
}
defer ctrl.Close()
if got, found := ctrl.CustomCommand("/handoff local"); !found || got != "PROJECT HANDOFF local" {
t.Fatalf("project short command=%q, %v", got, found)
}
for _, name := range []string{"handoff", "checks:summarize"} {
if _, found := ctrl.CustomCommand("/work-note-kit:" + name); found != present {
t.Fatalf("%s found=%v, want %v", name, found, present)
}
}
if !present {
if len(rec.Requests()) != 0 {
t.Fatal("discovering an absent template sent a provider request")
}
return
}
ctrl.EnsureSessionPath()
for _, input := range []string{
"/work-note-kit:handoff lint-passed integration-not-run",
"/work-note-kit:checks:summarize build unit-passed network-not-run",
"/work-note-kit:handoff",
} {
ctrl.Submit(input)
deadline := time.Now().Add(30 * time.Second)
for ctrl.Running() {
if time.Now().After(deadline) {
t.Fatal("prompt example turn did not finish")
}
time.Sleep(time.Millisecond)
}
}
requests := agentRequests(rec.Requests())
if len(requests) != 3 {
t.Fatalf("provider requests=%d, want three", len(requests))
}
for i, req := range requests {
var user string
for _, msg := range req.Messages {
if msg.Role == provider.RoleUser {
user = msg.Content
}
}
if strings.Contains(systemMessage(req.Messages), "Facts supplied by the caller:") || strings.Contains(systemMessage(req.Messages), "A literal dollar sign is") {
t.Fatal("template body entered the stable system prefix")
}
if i == 1 {
for _, want := range []string{"Label: build", "Observations: build unit-passed network-not-run", "A literal dollar sign is $."} {
if !strings.Contains(user, want) {
t.Fatalf("check template missing %q: %s", want, user)
}
}
} else {
if !strings.Contains(user, "Draft a short work handoff") || !strings.Contains(user, "name the missing facts") {
t.Fatalf("handoff instructions missing: %s", user)
}
if i == 0 && !strings.Contains(user, "Facts supplied by the caller:\nlint-passed integration-not-run") {
t.Fatalf("handoff arguments missing: %s", user)
}
if strings.Contains(user, "SOURCE UPDATE MARKER") != updated {
t.Fatalf("source update presence=%v, want %v", strings.Contains(user, "SOURCE UPDATE MARKER"), updated)
}
}
if strings.Contains(user, "$ARGUMENTS") || strings.Contains(user, "Label: $1") {
t.Fatal("unrendered template tokens reached the provider")
}
}
}
t.Run("absent", func(t *testing.T) { check(t, false, false) })
args := map[string]any{"source": source, "kind": "plugin", "mode": "copy", "scope": "global"}
preview := install(args)
if preview.Applied || preview.Status != "planned" || preview.PlanID == "" {
t.Fatalf("preview=%+v", preview)
}
t.Run("preview", func(t *testing.T) { check(t, false, false) })
args["apply"], args["planId"] = true, preview.PlanID
if applied := install(args); !applied.Applied || applied.Status != "done" {
t.Fatalf("applied=%+v", applied)
}
t.Run("copied", func(t *testing.T) { check(t, true, false) })
path := filepath.Join(source, "prompts", "handoff.md")
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(contents, []byte("\nSOURCE UPDATE MARKER\n")...), 0o644); err != nil {
t.Fatal(err)
}
t.Run("source-edit", func(t *testing.T) { check(t, true, false) })
args["replace"], args["apply"] = true, false
delete(args, "planId")
preview = install(args)
if preview.Applied || preview.Status != "planned" {
t.Fatalf("replacement preview=%+v", preview)
}
t.Run("replacement-preview", func(t *testing.T) { check(t, true, false) })
args["apply"], args["planId"] = true, preview.PlanID
if applied := install(args); !applied.Applied || applied.Status != "done" {
t.Fatalf("replacement=%+v", applied)
}
t.Run("replaced", func(t *testing.T) { check(t, true, true) })
for _, enabled := range []bool{false, true} {
if err := pluginpkg.SetEnabled(reasonixHome, "work-note-kit", enabled); err != nil {
t.Fatal(err)
}
if enabled {
t.Run("reenabled", func(t *testing.T) { check(t, true, true) })
} else {
t.Run("disabled", func(t *testing.T) { check(t, false, false) })
}
}
if removed := install(map[string]any{"op": "uninstall", "name": "work-note-kit", "scope": "global"}); !removed.Applied || removed.Status != "done" {
t.Fatalf("removed=%+v", removed)
}
if _, err := os.Stat(pluginpkg.InstallRoot(reasonixHome, "work-note-kit")); !os.IsNotExist(err) {
t.Fatalf("managed copy survives removal: %v", err)
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("removal deleted original source: %v", err)
}
t.Run("removed", func(t *testing.T) { check(t, false, false) })
}