diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 5c0e58f4..9200b415 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -7,6 +7,7 @@ on: permissions: contents: write + id-token: write jobs: release: @@ -71,8 +72,5 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish npm package - if: vars.PUBLISH_NPM == 'true' working-directory: cli/npm - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public --provenance diff --git a/.github/workflows/skills-drift.yml b/.github/workflows/skills-drift.yml new file mode 100644 index 00000000..450914ca --- /dev/null +++ b/.github/workflows/skills-drift.yml @@ -0,0 +1,32 @@ +name: Skills drift check + +on: + push: + paths: + - "templates/skills/**" + - "cli/hack/buildskills/**" + pull_request: + paths: + - "templates/skills/**" + - "cli/hack/buildskills/**" + - "skills/**" + - "cli/skills/**" + +jobs: + drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-go@v5 + with: + go-version-file: cli/go.mod + + - name: Regenerate skills + working-directory: cli + run: go generate ./... + + - name: Check for drift + run: git diff --exit-code skills/ cli/skills/ diff --git a/cli/README.md b/cli/README.md index 475609f2..8528024d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -6,10 +6,10 @@ A command-line tool for driving the [Subtext](https://subtext.fullstory.com) MCP ```bash # npx (no install required) -npx @fullstory/subtext-cli auth whoami +npx @subtextdev/subtext-cli auth whoami # npm (global install) -npm install -g @fullstory/subtext-cli +npm install -g @subtextdev/subtext-cli # go install go install github.com/fullstorydev/subtext/cli/cmd/subtext@latest @@ -31,7 +31,7 @@ subtext auth whoami subtext live connect --url https://example.com # 4. Take a screenshot -subtext live view screenshot +subtext live view-screenshot # 5. Create a proof document subtext doc create --title "My review" @@ -64,11 +64,11 @@ Tools follow the `subtext [--flags]` pattern: ```bash # Connect a browser and navigate subtext live connect --url https://example.com -subtext live view navigate --url https://example.com/dashboard +subtext live view-navigate --url https://example.com/dashboard # Create a document and attach a screenshot subtext doc create --title "Regression check" --tags p1 -subtext live view screenshot +subtext live view-screenshot subtext doc attach --session-url # JSON output (default) diff --git a/cli/RELEASING.md b/cli/RELEASING.md index 61de34e6..a104cdf4 100644 --- a/cli/RELEASING.md +++ b/cli/RELEASING.md @@ -3,7 +3,7 @@ ## Prerequisites - Write access to this repo (`fullstorydev/subtext`). -- (First release only) `@fullstory` npm scope access. Set `PUBLISH_NPM=true` on the repo once the scope is provisioned. +- (First release only) `@subtextdev` npm scope access and a Trusted Publisher configured on npmjs.com (see below). ## Release process @@ -46,13 +46,13 @@ Go to **Actions → Release CLI** in the repo. It will: - Builds binaries for darwin/linux/windows × amd64/arm64. - Creates tar.gz / zip archives and a `checksums.txt`. - Creates a GitHub Release named `CLI vX.Y.Z` under the `cli/v*` tag. -3. Publishes the npm package to `@fullstory/subtext-cli` (requires `PUBLISH_NPM=true` repo variable and `NPM_TOKEN` secret). +3. Publishes the npm package to `@subtextdev/subtext-cli` via OIDC (requires a Trusted Publisher configured on npmjs.com — no `NPM_TOKEN` secret needed). ### 5. Smoke test ```bash # via npm -npx @fullstory/subtext-cli@0.2.0 --version +npx @subtextdev/subtext-cli@0.2.0 --version # via go install (requires cli/v0.2.0 tag to be indexed by the Go module proxy) go install github.com/fullstorydev/subtext/cli/cmd/subtext@v0.2.0 @@ -83,6 +83,6 @@ No tag or GitHub credentials needed. Binaries land in `cli/dist/`. ## First release checklist -- [ ] Confirm `@fullstory` npm scope exists and `NPM_TOKEN` secret is set in repo settings. -- [ ] Set `PUBLISH_NPM=true` as a repo variable in GitHub Actions settings. -- [ ] Test `npx @fullstory/subtext-cli auth whoami` after publish. +- [ ] Confirm `@subtextdev` npm scope exists on npmjs.com. +- [ ] Configure a Trusted Publisher on npmjs.com: GitHub Actions, org `fullstorydev`, repo `subtext`, workflow `release-cli.yml`, allow `npm publish`. +- [ ] Test `npx @subtextdev/subtext-cli auth whoami` after publish. diff --git a/cli/generate.go b/cli/generate.go new file mode 100644 index 00000000..2db64413 --- /dev/null +++ b/cli/generate.go @@ -0,0 +1,5 @@ +// Package cli contains build-time helpers for this module. +// The //go:generate directive below regenerates the skills output trees. +package cli + +//go:generate go run ./hack/buildskills diff --git a/cli/hack/buildskills/main.go b/cli/hack/buildskills/main.go new file mode 100644 index 00000000..4ed32301 --- /dev/null +++ b/cli/hack/buildskills/main.go @@ -0,0 +1,385 @@ +// buildskills generates skills/ (MCP) and cli/skills/ (CLI) from templates/skills/. +// Run via: go generate ./... from the cli/ directory. +package main + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "regexp" + "slices" + "sort" + "strings" + "text/template" +) + +// toolNamespaces is the allowlist for {{tool "name"}} references. +var toolNamespaces = []string{ + "live", "comment", "doc", "tunnel", "review", "sightmap", "artifact", "auth", +} + +func main() { + // Paths are relative to cli/, where go generate runs. + templatesDir := filepath.Join("..", "templates", "skills") + targetDirs := map[string]string{ + "mcp": filepath.Join("..", "skills"), + "cli": "skills", + } + + entries, err := os.ReadDir(templatesDir) + if err != nil { + fatal("read templates dir: %v", err) + } + + // Track which skill dirs we write per target, to clean up stale ones. + written := map[string][]string{"mcp": nil, "cli": nil} + + for _, e := range entries { + if !e.IsDir() { + continue + } + skillName := e.Name() + skillDir := filepath.Join(templatesDir, skillName) + + skillFile := filepath.Join(skillDir, "SKILL.md") + if _, err := os.Stat(skillFile); os.IsNotExist(err) { + continue // skip dirs without a SKILL.md (e.g. placeholder dirs) + } + fm, body, err := readSkill(skillFile) + if err != nil { + fatal("skill %s: %v", skillName, err) + } + + skillTargets := fm.targets + if len(skillTargets) == 0 { + skillTargets = []string{"mcp", "cli"} + } + + for _, target := range skillTargets { + outDir, ok := targetDirs[target] + if !ok { + fatal("skill %s: unknown target %q", skillName, target) + } + outSkillDir := filepath.Join(outDir, skillName) + + // Use SKILL..md body if present. + bodyToRender := body + overrideFile := filepath.Join(skillDir, "SKILL."+target+".md") + if data, err := os.ReadFile(overrideFile); err == nil { + bodyToRender = string(data) + } + + rendered, err := renderTemplate(bodyToRender, skillName, target) + if err != nil { + fatal("skill %s target %s: render: %v", skillName, target, err) + } + + outFM := fm.forTarget(target) + content := "---\n" + outFM + "---\n" + rendered + + if err := os.MkdirAll(outSkillDir, 0o755); err != nil { + fatal("mkdir %s: %v", outSkillDir, err) + } + if err := os.WriteFile(filepath.Join(outSkillDir, "SKILL.md"), []byte(content), 0o644); err != nil { + fatal("write skill %s/%s: %v", target, skillName, err) + } + + // Copy sibling files (non-SKILL*.md). + if err := copySiblings(skillDir, outSkillDir, target); err != nil { + fatal("copy siblings %s/%s: %v", target, skillName, err) + } + + written[target] = append(written[target], skillName) + } + } + + // Remove stale output dirs no longer present in templates. + for target, outDir := range targetDirs { + existingDirs, _ := readDirNames(outDir) + for _, d := range existingDirs { + if d == "embed.go" { + continue + } + if !slices.Contains(written[target], d) { + if err := os.RemoveAll(filepath.Join(outDir, d)); err != nil { + fatal("remove stale %s/%s: %v", target, d, err) + } + } + } + } + + // Write cli/skills/embed.go listing the CLI skill directories. + if err := writeEmbedFile("skills", written["cli"]); err != nil { + fatal("write embed.go: %v", err) + } + + fmt.Printf("buildskills: wrote %d MCP skills, %d CLI skills\n", + len(written["mcp"]), len(written["cli"])) +} + +// skill holds parsed SKILL.md data. +type skill struct { + rawFrontmatter string // original YAML between the --- delimiters + name string // extracted from name: field + targets []string +} + +// targetsRe matches " targets: [mcp, cli]" or " targets: [mcp]" etc. +var targetsRe = regexp.MustCompile(`(?m)^\s*targets:\s*\[([^\]]*)\]`) + +// nameRe matches "name: foo" in frontmatter. +var nameRe = regexp.MustCompile(`(?m)^name:\s*(\S+)`) + +// readSkill reads SKILL.md and splits into skill struct and body string. +func readSkill(path string) (*skill, string, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, "", err + } + content := string(data) + parts := strings.SplitN(content, "---", 3) + if len(parts) < 3 { + return nil, "", fmt.Errorf("missing frontmatter delimiters in %s", path) + } + fmRaw := parts[1] + + // Extract name. + nm := nameRe.FindStringSubmatch(fmRaw) + if nm == nil { + return nil, "", fmt.Errorf("frontmatter missing name field in %s", path) + } + + // Extract targets. + var targets []string + if tm := targetsRe.FindStringSubmatch(fmRaw); tm != nil { + for _, t := range strings.Split(tm[1], ",") { + t = strings.TrimSpace(t) + if t != "" { + targets = append(targets, t) + } + } + } + + return &skill{ + rawFrontmatter: fmRaw, + name: nm[1], + targets: targets, + }, parts[2], nil +} + +// forTarget returns the frontmatter YAML string for the output file. +// It strips the `targets:` line and injects `_generated_from:`. +func (s *skill) forTarget(target string) string { + lines := strings.Split(s.rawFrontmatter, "\n") + var out []string + inMetadata := false + metadataIndent := "" + generatedFromInserted := false + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + // Skip the targets line. + if targetsRe.MatchString(line) { + continue + } + + // Detect "metadata:" block. + if strings.HasPrefix(trimmed, "metadata:") { + inMetadata = true + metadataIndent = strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) + out = append(out, line) + // Inject _generated_from as first child. + out = append(out, metadataIndent+" _generated_from: templates/skills/"+s.name+"/SKILL.md") + generatedFromInserted = true + continue + } + + // Once in metadata, check if we've left it (dedent or new top-level key). + if inMetadata && trimmed != "" && !strings.HasPrefix(line, metadataIndent+" ") { + inMetadata = false + } + + out = append(out, line) + } + + if !generatedFromInserted { + // No metadata block exists — add one. + // Insert before the closing blank line / end. + out = append(out, "metadata:") + out = append(out, " _generated_from: templates/skills/"+s.name+"/SKILL.md") + } + + return strings.Join(out, "\n") +} + +// renderTemplate processes the skill body through text/template. +func renderTemplate(body, skillName, target string) (string, error) { + funcs := template.FuncMap{ + "tool": func(name string) (string, error) { + return expandTool(name, target) + }, + "cli": func(s string) string { + if target == "cli" { + return s + } + return "" + }, + "mcp": func(s string) string { + if target == "mcp" { + return s + } + return "" + }, + } + data := struct{ Target string }{Target: target} + tmpl, err := template.New(skillName).Funcs(funcs).Parse(body) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// expandTool converts a tool name to its display form for the given target. +// MCP: `tool-name` +// CLI: `subtext ns rest-of-name` (split on first hyphen) +func expandTool(name, target string) (string, error) { + parts := strings.SplitN(name, "-", 2) + ns := parts[0] + if !slices.Contains(toolNamespaces, ns) { + return "", fmt.Errorf("tool %q: unknown namespace %q (allowed: %v)", name, ns, toolNamespaces) + } + if target == "cli" { + if len(parts) == 2 { + return "`subtext " + ns + " " + parts[1] + "`", nil + } + return "`subtext " + ns + "`", nil + } + return "`" + name + "`", nil +} + +// copySiblings copies non-SKILL*.md sibling files to the output dir. +// Files in _/ are copied to outDir (stripping the _/ prefix). +// Other siblings (Python scripts, testdata, etc.) go to MCP output only. +func copySiblings(srcDir, outDir, target string) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + for _, e := range entries { + name := e.Name() + if strings.HasPrefix(name, "SKILL") && strings.HasSuffix(name, ".md") { + continue + } + + srcPath := filepath.Join(srcDir, name) + + // _mcp/ and _cli/ subdirs are target-specific. + if e.IsDir() && name == "_"+target { + if err := copyDir(srcPath, outDir); err != nil { + return fmt.Errorf("copy _%s/: %w", target, err) + } + continue + } + if e.IsDir() && strings.HasPrefix(name, "_") { + continue // skip the other target's dir + } + + // All other siblings are MCP-only by default. + if target != "mcp" { + continue + } + if e.IsDir() { + if err := copyDir(srcPath, filepath.Join(outDir, name)); err != nil { + return err + } + } else { + if err := copyFile(srcPath, filepath.Join(outDir, name)); err != nil { + return err + } + } + } + return nil +} + +func copyDir(src, dst string) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, _ := filepath.Rel(src, path) + dstPath := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(dstPath, 0o755) + } + return copyFile(path, dstPath) + }) +} + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + return err +} + +func readDirNames(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + names := make([]string, 0, len(entries)) + for _, e := range entries { + if e.IsDir() { + names = append(names, e.Name()) + } + } + return names, nil +} + +// writeEmbedFile writes cli/skills/embed.go with a //go:embed directive +// listing the given skill directory names. +func writeEmbedFile(skillsDir string, dirs []string) error { + if len(dirs) == 0 { + return nil + } + sort.Strings(dirs) + var sb strings.Builder + sb.WriteString("// Code generated by cli/hack/buildskills. DO NOT EDIT.\n\n") + sb.WriteString("package skills\n\n") + sb.WriteString("import \"embed\"\n\n") + sb.WriteString("// FS contains the embedded CLI skill documentation.\n") + sb.WriteString("//go:embed") + for _, d := range dirs { + sb.WriteString(" ") + sb.WriteString(d) + } + sb.WriteString("\nvar FS embed.FS\n") + return os.WriteFile(filepath.Join(skillsDir, "embed.go"), []byte(sb.String()), 0o644) +} + +func fatal(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "buildskills: "+format+"\n", args...) + os.Exit(1) +} diff --git a/cli/npm/bin/subtext.js b/cli/npm/bin/subtext.js index 11c19394..a0e7818e 100644 --- a/cli/npm/bin/subtext.js +++ b/cli/npm/bin/subtext.js @@ -17,7 +17,7 @@ const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" }); if (result.error) { if (result.error.code === "ENOENT") { console.error( - "subtext: binary not found. Try reinstalling: npm install @fullstory/subtext-cli" + "subtext: binary not found. Try reinstalling: npm install @subtextdev/subtext-cli" ); } else { console.error("subtext:", result.error.message); diff --git a/cli/npm/package.json b/cli/npm/package.json index 42ef1c34..f1361c49 100644 --- a/cli/npm/package.json +++ b/cli/npm/package.json @@ -1,6 +1,6 @@ { - "name": "@fullstory/subtext-cli", - "version": "0.1.0", + "name": "@subtextdev/subtext-cli", + "version": "1.0.0", "description": "Subtext CLI — drive the Subtext MCP server from your terminal", "license": "MIT", "homepage": "https://subtext.fullstory.com", diff --git a/cli/skills/comments/SKILL.md b/cli/skills/comments/SKILL.md new file mode 100644 index 00000000..5461a644 --- /dev/null +++ b/cli/skills/comments/SKILL.md @@ -0,0 +1,104 @@ +--- + +name: comments +description: Comment MCP tools for agent-user collaboration. Use when reviewing sessions or live pages to leave observations, read user feedback, reply, and resolve. +metadata: + _generated_from: templates/skills/comments/SKILL.md + requires: + skills: ["subtext:shared"] +--- + + +# Comments + +> **PREREQUISITE:** Read `subtext:shared` for conventions and sightmap upload. + +Tool catalog and judgment rules for comment-based agent-user collaboration. Comment commands are available in the `subtext` CLI. + +## Commands + +| Tool | Description | +|------|-------------| +| `subtext comment list` | Read all comments/annotations on a trace, with thread structure | +| `subtext comment add` | Leave a comment on a trace, optionally tied to a page and timestamp | +| `subtext comment reply` | Reply to an existing comment by ID | +| `subtext comment resolve` | Mark a comment thread as resolved | + +All comment tools are **stateless** — they identify the parent trace by `trace_id` (preferred) or `session_id` (deprecated; in `deviceId:sessionId` format), rather than requiring an active connection. + +### `trace_id` vs `session_id` + +Comments hang off a **trace** — the durable parent identifier that survives even when no FullStory session was captured. Every tool that needs a parent accepts either: + +- `trace_id` — the 12-char base62 id you get from `subtext live connect` (`trace_id:` line, or parse the trailing path of `trace_url`). **Prefer this.** It's stable, works for traces with no underlying FS session, and is the only key the storage layer actually uses. +- `session_id` — the legacy `deviceId:sessionId` form. Still accepted for callers that only have an FS session URL on hand. The server promotes it to a trace_id under the hood. Responses include a one-line deprecation hint when you use this path. + +`subtext comment resolve` only needs `comment_id`; the parent is looked up server-side. + +## Discovering Parameters + +Parameter schemas are visible in the tool definition at call time. + +## Screenshots + +Comment tools do **not** auto-capture screenshots. To attach a screenshot, pass a `screenshot_url` to `subtext comment add`. This URL must point to a pre-captured screenshot (e.g., from `subtext live view-screenshot` or another source). + +> **Note:** To attach a screenshot, first capture one via `subtext live view-screenshot`, then pass the returned URL as `screenshot_url` **verbatim** — the signed query string (`?Expires=…&GoogleAccessId=…&Signature=…`) is the credential. Stripping it returns 403 from GCS and the image won't render. + +When the comment is about a specific element, capture a focused clip by passing `component_id` (and a small `expand_pct` for context) to the screenshot tool. A focused clip is far more useful in a comment than a full viewport — the reader sees exactly what you're pointing at. + +## Markdown + +Comment text is rendered as **Markdown** in the comment thread UI. Use standard formatting freely: + +- **Bold** / *italic* for emphasis +- Bulleted and numbered lists for structured observations +- `code spans` and fenced code blocks for selectors, error messages, or snippets +- [Links](url) to reference external evidence or docs + +Keep formatting proportional to the comment — a one-line observation doesn't need a bulleted list. + +## Intents + +When adding a comment, classify it: + +| Intent | Use when | +|--------|----------| +| `bug` | Something is broken | +| `tweak` | Minor improvement needed | +| `ask` | Question for user or another agent | +| `looks-good` | Confirming an area passes review | + +## Rules + +1. **List before acting.** Every time you receive a session URL or page URL, `subtext comment list` first. Never assume you know what feedback exists. +2. **Reply before resolving.** The reply is the audit trail. A silent resolve is invisible history. +3. **Agents resolve their own observations freely** after visual verification confirms the fix. +4. **User comments: reply with status, let users resolve** — unless they explicitly say "resolve it" or you have screenshot proof the specific issue is gone. +5. **Agent-to-agent handoffs:** Read prior agent's comments via `subtext comment list`, reply to acknowledge before starting your own work, don't resolve another agent's comments without verifying. + +## Review Handoff Loop + +Comments enable asynchronous review between agents and users: + +1. Agent does work → runs visual verification +2. Agent calls `subtext comment add` with observations (`bug`, `tweak`, `looks-good`) +3. Agent shares the trace URL with the user +4. User reviews → reads agent comments → leaves own comments/replies +5. User shares URL back to agent +6. Agent calls `subtext comment list` to read ALL feedback +7. Agent addresses each issue → `subtext comment reply` with status +8. Agent calls `subtext comment resolve` ONLY on verified fixes +9. Repeat from step 4 until user is satisfied + +## Gotchas + +- Forgetting to `subtext comment list` on entry — you'll duplicate work or miss user feedback +- Resolving user comments without replying — no audit trail, user doesn't know what happened +- Resolving without visual verification — "I fixed it" without a screenshot is a claim, not evidence +- Adding comments without navigating to the relevant page first — comments attach to what's currently visible + +## See Also + +- `subtext:shared` — MCP conventions and sightmap upload + diff --git a/cli/skills/embed.go b/cli/skills/embed.go new file mode 100644 index 00000000..779cd3c2 --- /dev/null +++ b/cli/skills/embed.go @@ -0,0 +1,9 @@ +// Code generated by cli/hack/buildskills. DO NOT EDIT. + +package skills + +import "embed" + +// FS contains the embedded CLI skill documentation. +//go:embed comments docs live proof recipe-sightmap-setup shared sightmap tunnel +var FS embed.FS diff --git a/cli/skills/embed_test.go b/cli/skills/embed_test.go new file mode 100644 index 00000000..2deb8b8a --- /dev/null +++ b/cli/skills/embed_test.go @@ -0,0 +1,32 @@ +package skills_test + +import ( + "io/fs" + "testing" + + "github.com/fullstorydev/subtext/cli/internal/fstesting" + "github.com/fullstorydev/subtext/cli/skills" +) + +// TestEmbeddedSkills verifies that every skill directory in the embedded FS +// contains a readable SKILL.md. This catches a stale embed.go (e.g. a skill +// was added to templates/skills/ but go generate was not re-run). +func TestEmbeddedSkills(t *testing.T) { + dirs, err := fs.ReadDir(skills.FS, ".") + fstesting.Ok(t, err, "read embedded FS root") + + fstesting.Assert(t, len(dirs) > 0, "embedded FS should contain at least one skill directory") + + for _, d := range dirs { + if !d.IsDir() { + continue + } + name := d.Name() + t.Run(name, func(t *testing.T) { + skillPath := name + "/SKILL.md" + f, err := skills.FS.Open(skillPath) + fstesting.Ok(t, err, "open %s", skillPath) + f.Close() + }) + } +} diff --git a/cli/skills/live/SKILL.md b/cli/skills/live/SKILL.md new file mode 100644 index 00000000..5db7e923 --- /dev/null +++ b/cli/skills/live/SKILL.md @@ -0,0 +1,166 @@ +--- + +name: live +description: Live browser MCP tools for driving a hosted browser — connections, views, interactions, console, network, and tunnel. Use when reproducing flows, taking screenshots, or interacting with a running app. +metadata: + _generated_from: templates/skills/live/SKILL.md + requires: + skills: ["subtext:shared"] +--- +# Live Browser + +> **PREREQUISITE:** Read `subtext:shared` for conventions and sightmap upload. + +Command catalog for live browser commands (`subtext live *`). These commands let you open browser connections, navigate views, interact with elements, and inspect console/network activity. + +## Commands + +### Connections + +| Command | Description | +|---------|-------------| +| `subtext live connect` | Open a browser connection to a URL. Returns screenshot, component tree, `fs_session_url`, `trace_id`, `trace_url`, and `capture_status`. | +| `subtext live disconnect` | Close a browser connection. Returns `fs_session_url`, `trace_id`, and `trace_url`. | +| `subtext live emulate` | Set device emulation (viewport, user agent, etc.) | + +### Views + +| Command | Description | +|---------|-------------| +| `subtext live view-navigate` | Navigate the current view to a new URL | +| `subtext live view-new` | Open a new view (tab) | +| `subtext live view-list` | List all open views | +| `subtext live view-select` | Switch to a different view | +| `subtext live view-close` | Close a view | +| `subtext live view-snapshot` | Component tree snapshot (no screenshot) | +| `subtext live view-inspect` | Component tree with full CSS selectors — for sightmap authoring only, not general use | +| `subtext live view-screenshot` | Visual screenshot of current view. Pass `component_id` to clip to a specific element's bounding box; optional `expand_pct` (0–100) grows the clip rect outward for surrounding context, clamped to the viewport. | +| `subtext live view-resize` | Resize the viewport | + +### Interactions + +| Command | Description | +|---------|-------------| +| `subtext live act-click` | Click an element by UID | +| `subtext live act-hover` | Hover over an element | +| `subtext live act-fill` | Fill a text input | +| `subtext live act-keypress` | Press a key or key combination | +| `subtext live act-drag` | Drag from one element to another | +| `subtext live act-scroll` | Scroll the view: by component UID (into view), pixel delta, or absolute position | +| `subtext live act-wait-for` | Wait for a condition (selector, navigation, timeout) | +| `subtext live act-dialog` | Accept or dismiss a browser dialog | +| `subtext live act-upload` | Upload a file to a file input | + +### Developer Tools + +| Command | Description | +|---------|-------------| +| `subtext live eval-script` | Run JavaScript in the page context | +| `subtext live log-list` | List console messages | +| `subtext live log-get` | Get details of a specific console message | +| `subtext live net-list` | List network requests | +| `subtext live net-get` | Get details of a specific network request | + +### Signals + +| Command | Description | +|---------|-------------| +| `subtext live signal` | Poll the trace for operator state and new comment signals. Returns `{operator, operator_email?, signals[], cursor, server_time}`. Cursor-based — pass `since` back on the next call. | + +### Tunnel + +| Command | Description | +|---------|-------------| +| `subtext live tunnel` | Get tunnel relay URL for connecting to localhost | + +## Discovering Parameters + +Run `subtext live --help` to see parameters for any command. + +## Trace and Session URLs + +`fs_session_url`, `trace_id`, and `trace_url` are returned by `subtext live connect`, `subtext live disconnect`, `subtext live view-navigate`, `subtext live view-new`, and `subtext live view-snapshot`. + +- **fs_session_url** — the raw Fullstory session URL. +- **trace_id** — the 12-char base62 id for this connection's trace. **Capture and reuse this** as the parent identifier for `subtext comment *` commands. The trailing path segment of `trace_url` is the same id. +- **trace_url** — a shareable link that opens the live viewer in a browser. **Always print this to the user** so they can watch the agent's browser in real time. + +After every connection is established — via `subtext live connect` or `subtext live view-new` (tunnel-first flow) — output the URL on its own line: + +``` +Viewer: {trace_url} +``` + +## Capture Status + +Every live command response that touches a view includes a `capture_status` field — +this includes `subtext live connect`, `subtext live view-new`, `subtext live view-navigate`, +`subtext live view-snapshot`, and `subtext live view-screenshot`. Check it after **every** such +call (not just `subtext live connect`) and respond as follows: + +- `active`: proceed normally. +- `blocked`: tell the user to check capture quota and verify the target domain is allow listed in Subtext data capture settings. +- `snippet_not_found` or `api_unavailable`: tell the user something went wrong during setup and they should check their API key and endpoint configuration. +- any other status: something went wrong, try again + +This matters especially in the **tunnel-first flow**, where `subtext live connect` is +never called — it's easy to miss the status if you assume the check only applies +to that one tool. + +## Operator and Signals + +`subtext live signal` is the trace's read channel for human-side activity. Call it +between action loops to learn about new comments and the operator state. + +**Response shape:** + +```json +{ + "operator": "agent" | "human", + "operator_email": "...", // present when operator=human + "signals": [ + { + "type": "comment", "id": "...", "ts": "...", + "text": "...", "author_type": "user|agent", + "intent": "...", "resolved": false, "parent_id": "..." + } + ], + "cursor": "...", // round-trip back as `since` next call + "server_time": "..." +} +``` + +**Polling pattern.** + +1. First call after `subtext live connect`: omit `since` to baseline the cursor. +2. Subsequent calls: pass the previous response's `cursor` as `since`. Only signals newer than the cursor come back. +3. Comment signals carry full text and metadata inline — no follow-up `subtext comment list` is needed for the new ones. +4. Save the new `cursor` after every call. + +**Operator gate.** When `operator=human`, the user has taken browser control. The `live-act-*` input commands (click, fill, hover, keypress, drag, dialog, upload) return a structured error and **must not be retried**. Read-only commands (`subtext live view-snapshot`, `subtext live view-screenshot`, `live-log-*`, `live-net-*`, `subtext live signal`) keep working. Stay read-only and keep polling — when `operator` flips back to `agent`, resume normal work. + +`subtext live act-wait-for` is excluded from the gate (observation-only). + +## Tips + +- **Default to `subtext live view-snapshot` for all page observation.** It carries sightmap context (component names, view names, `[src: ...]` annotations) and provides the component UIDs needed by `act-*` commands. Always call it before any interaction sequence. +- **Use `subtext live view-screenshot` only for visual evidence** — before/after comparisons, layout debugging, or screenshots to embed in PRs. The command returns the image inline by default so you can verify framing. Pass `--upload` only when you need a hosted URL for a PR or comment attachment. +- When the screenshot is evidence about a specific element, clip to it with `--component-id` (and small `--expand-pct` for context). `expand_pct` caps at 100, so very short elements (a label, a textbox) still produce thin slices — clip to a wider parent in that case. +- `subtext live view-inspect` is for **sightmap authoring only** — it returns verbose CSS selectors on every node. Do not use it as a general snapshot replacement. Use it once to discover selectors, write your `.sightmap/` YAML, then use `subtext live view-snapshot` for everything else. +- Component names from sightmap appear in snapshots — use `[src: ...]` annotations to find source files. +- Close connections when done to free server resources. + +## Tunnel Setup + +When the hosted browser needs to reach `localhost` or local dev URLs, use the tunnel-first flow: + +1. Run `subtext live tunnel` — allocates a browser connection and returns `relayUrl` + `connectionId` +2. Run `subtext tunnel connect` with `relayUrl` and `allowedOrigins` (one or more local origins the tunnel may serve) +3. Run `subtext live view-new` with the `connection_id` and localhost URL + +Do **not** use `subtext live connect` for localhost URLs — it mints its own connection ID and can't bind to the tunnel. See `subtext:tunnel` for full details, including the trunk pattern (`host:port` covers all subdomains on the same port) needed when local apps redirect across subdomains (e.g. OAuth flows). + +## See Also + +- `subtext:shared` — shared conventions and sightmap upload +- `subtext:tunnel` — Reverse tunnel setup for localhost access diff --git a/cli/skills/proof/SKILL.md b/cli/skills/proof/SKILL.md new file mode 100644 index 00000000..3b3c677d --- /dev/null +++ b/cli/skills/proof/SKILL.md @@ -0,0 +1,291 @@ +--- + +name: proof +description: You MUST use this skill when implementing, fixing, or refactoring code. Captures evidence artifacts (screenshots, network traces, code diffs, trace session links) into a proof document as you work. +metadata: + _generated_from: templates/skills/proof/SKILL.md + requires: + skills: ["subtext:shared", "subtext:live", "subtext:comments", "subtext:docs"] + mcp-server: subtext +--- + + +# Proof + +> **PREREQUISITE — Read inline before any other action:** Read skills `subtext:shared`, `subtext:live`, `subtext:comments`, `subtext:docs`. + +**Type:** Rigid workflow — follow exactly. Skipping steps means unverified work ships. + +Every code change that affects what a user sees must be visually proven. This skill creates a before/after evidence trail that proves the change works. No exceptions, no "it should look fine." + +## Screenshot Capture + +**Always use `subtext live view-screenshot` with `upload: true`.** This uploads the screenshot to cloud storage and returns a signed URL you can attach to comments and PRs. + + +```bash +subtext live view-screenshot --upload +# → { "data": { "screenshot_url": "https://..." } } +``` + + +**Do NOT use `subtext artifact upload` for screenshots.** It requires base64-encoding the entire PNG and frequently fails on large images. The `upload: true` flag on `subtext live view-screenshot` handles the upload server-side — smaller payload, no encoding issues. + +**Pass `screenshot_url` through verbatim — query string included.** The full signed URL is the credential. Don't strip `?Expires=…&GoogleAccessId=…&Signature=…` when copying it into PR descriptions, comments, or summaries — without those params GCS returns 403 and the image won't render. + +To attach a screenshot to a comment: + + +```bash +URL=$(subtext live view-screenshot --upload | jq -r '.data.screenshot_url') +subtext comment add --screenshot_url "$URL" --intent looks-good --text "AFTER: ..." +``` + + +## Proof Document + +Every proof run creates a permanent record alongside the live session. This lets you — and any future reviewer — reconstruct exactly what changed, what it looked like before and after, and what evidence backed the decision to ship. + +**Create once, attach continuously, close at the end.** Pass the `verification` seed template (see `subtext:docs`) unless you have a better fit: + + +```bash +subtext doc create --title "" --content "" +# → doc_id, doc_url ← save both +``` + + +The `doc_id` travels through every step below. The `doc_url` is the permanent link you hand to the user at the end. + +## The Loop + +### Step 1: Connect to the running app + +Open a browser connection per `subtext:live`'s connect flow — `subtext live connect` for remote URLs, tunnel-first (`subtext live tunnel` → `subtext tunnel connect` → `subtext live view-new`) for localhost. The hosted browser cannot reach localhost without the tunnel; see `subtext:live` for both flows in detail. + +If the app isn't running, **try to start the dev server yourself first.** Look for `package.json` scripts (`dev`, `start`, `serve`), a `Makefile`, or a `docker-compose.yml`. Run the appropriate command in the background. Only ask the user if you can't figure out how to start it. + +Read any existing comments with `subtext comment list` — prior feedback may inform your work. + +Call `subtext doc create` with the `verification` seed template (or `bug-fix` / `changeset` if the context fits better). Save `doc_id` and `doc_url`. + +### Step 2: Share the trace URL + +**Immediately** print the `trace_url` from the connect step on its own line. (`subtext live connect` returns it for remote; `subtext live view-new` returns it for tunnel-first.) This lets the user watch the agent's browser in real time and gives downstream reviewers a stable entry point to the recorded session. + +``` +Trace: {trace_url} +I'm connected to the app. Starting verification. +``` + +Do NOT bury the link in a wall of text. It goes first, on its own line. + +### Polling discipline (Steps 3–6) + +While the trace is open, the human reviewer can leave comments or take browser control at any time. Between any two `live-*` calls in the loop below, call `subtext live signal` with the cursor saved from the previous call (omit `since` on the first call to baseline). New comments come back inline — read each, reply via `subtext comment reply` if it directs the work, and save the new `cursor`. The `operator` field on every response is the source of truth for control state. See `subtext:live` for the full response shape and operator-gate behavior. + +### Step 3: Navigate to the affected area and capture BEFORE + +Drive the browser to the exact page/component/state where the change will be visible. + +1. Navigate to the right URL, click through to the right state +2. Call `subtext live view-screenshot` with `upload: true` — this is the BEFORE evidence. Save the returned `screenshot_url`. +3. Call `subtext comment add` with intent `ask`, passing the `screenshot_url`: + - Text: "BEFORE: [describe current state]. About to make [describe planned change]." + - This is a chapter marker — it anchors the timeline for anyone reviewing the session later. +4. Attach to the proof document: + + ```bash + subtext doc attach --doc_id --section "Before" --render_as image --url --label "Before: " + ``` + + +**Judgment call:** If the change affects multiple pages or states, capture BEFORE for each. + +### Step 4: Make the code change + +Edit files, update components, fix styles — whatever the task requires. + +This is the only step where you leave the browser and work in the codebase. + +After editing, attach the diff to the proof document: + +```bash +subtext doc attach --doc_id --section "Changes" --render_as link \ + --text "$(git diff)" --content_type text/plain \ + --label "" +``` + + +If a `live-act-*` tool returns `Control transferred to human viewer`, the reviewer has taken control. Enter standby — do not retry. Continue polling `subtext live signal`; when `operator` flips back to `agent`, resume UI-facing work. Backend changes that don't need visual verification can continue while you wait. + +### Step 5: Verify the change with live tools + +Return to the browser. Refresh, hot reload, or reconnect if the dev server restarted. + +1. Navigate back to the same page/state from Step 3 +2. Call `subtext live view-screenshot` with `upload: true` — this is the AFTER evidence. Save the returned `screenshot_url`. +3. **Visually compare** BEFORE vs AFTER against the original prompt intent or acceptance criteria +4. Call `subtext comment add` with the AFTER `screenshot_url`: + - Intent: `looks-good` if it matches intent, `bug` if something is wrong + - Text: "AFTER: [describe what changed]. [Assessment against acceptance criteria]." +5. Attach to the proof document: + + ```bash + subtext doc attach --doc_id --section "After" --render_as image --url --label "After: " + ``` + + +**Use live interaction tools to test the change:** + +- Click buttons, fill forms, hover elements — confirm the change works functionally, not just visually +- Check edge cases: empty states, long text, missing data +- If the change touches styles: check dark mode, mobile viewport (use `subtext live emulate`) + +### Step 6: Self-correct if needed + +If the AFTER doesn't match intent: + +1. Call `subtext live view-screenshot` with `upload: true`, then `subtext comment add` with intent `bug` and the `screenshot_url`: "ISSUE: [what's wrong]. Fixing now." +2. Attach the issue screenshot to the proof document: + + ```bash + subtext doc attach --doc_id --section "After" --render_as image --url --label "Issue: " + ``` + +3. Go back to Step 4, make the fix +4. Return to Step 5, re-verify + +**Max 5 iterations.** If you can't get it right in 5 tries, stop and share what you have with the user. Don't spin. + +### Step 7: Package evidence and close the proof document + +Once the change is verified: + +1. Take a final `subtext live view-screenshot` with `upload: true` of the confirmed state +2. Call `subtext comment add` with intent `looks-good` and the `screenshot_url`: "VERIFIED: [summary of what was changed and confirmed]." +3. Attach the trace to the proof document: + + ```bash + subtext doc attach --doc_id --section "Evidence" --render_as link --url --label "Session trace" + ``` + +4. Re-read the document (`subtext doc read`) and confirm a cold reviewer would understand what changed, what was tested, and why. Attach anything missing. +5. Close: + + ```bash + subtext doc close --doc_id --status complete --summary "" + ``` + +6. If a PR exists or will be created: + - Include before/after screenshot URLs (from Step 3 and Step 5) in the PR description + - Include the `trace_url` and `doc_url` so reviewers can watch the session and read the evidence record + +```markdown +## Visual Evidence + +**Before:** +![before]({before_screenshot_url}) + +**After:** +![after]({after_screenshot_url}) + +**Session replay:** [Review the full session]({trace_url}) +**Proof document:** {doc_url} +``` + +The `screenshot_url` values are signed URLs from `subtext live view-screenshot` with `upload: true`. They render directly in GitHub PR descriptions as inline images. + +## When to Use This Skill + +**Always use when you modify:** + +- Component files: `.tsx`, `.jsx`, `.vue`, `.svelte` +- Stylesheets: `.css`, `.scss`, `.less`, `.tailwind` +- Template/markup: `.html`, `.ejs` +- Any file that changes what renders on screen + +**Skip when:** + +- Change is purely backend (API handler, database query, utility function) +- Change is test-only (no production UI impact) +- Change is documentation-only + +**Not sure?** Use the skill. The cost of an unnecessary screenshot is near zero. The cost of shipping an unverified visual change is a bug report. + +## Comment Annotations as Chapter Markers + +Comments you leave during verification serve two purposes: + +1. **Live collaboration** — the user watching the trace URL sees your annotations in real-time in the sidebar +2. **Session replay chapters** — anyone reviewing the recorded session later can jump to your BEFORE/AFTER markers to understand what changed and why + +Leave comments at every significant moment: + +- BEFORE state captured +- Change made, verifying now +- ISSUE found (with screenshot) +- VERIFIED (with screenshot) + +Think of these as commit messages for visual state. + +## Decision Logic + +### Functional vs. aesthetic changes + +| Change type | Verification depth | Share with user? | +| ----------------------------------------------------------- | ------------------------------------------------------- | --------------------------------- | +| Functional fix (broken button, wrong text, missing handler) | Self-verify: BEFORE/AFTER screenshots + functional test | Only if it fails after 2 attempts | +| Aesthetic change (new component, colors, spacing, layout) | Full verify: BEFORE/AFTER + dark mode + mobile viewport | Always — aesthetic is subjective | +| Style-touching (CSS variables, theme classes, responsive) | Full verify + theme variants + viewport variants | Always — high regression risk | + +### Multiple affected areas + +If the change affects more than one page or state: + +1. List all affected areas +2. BEFORE screenshot each one +3. Make the change +4. AFTER screenshot each one +5. Any failure = back to Step 4 + +## Composition + +- **Requires:** `subtext:live` (browser tools, returns `trace_url`), `subtext:comments` (annotations), `subtext:docs` (proof document) +- **Triggers from:** any file edit to UI code, or when the user asks for a visual change + +## Async heartbeat (Claude Code only, MANDATORY) + +The polling discipline above keeps you in sync as long as you're actively +calling `live-*` tools. Long idle gaps inside a proof run — multi-minute +compilations, deep code work without browser tool calls, waiting on a build — +can leave the agent unaware of comments or control changes that arrived during +the gap. + +On Claude Code, schedule an async heartbeat with `/loop` to cover that case: + +``` +/loop 60s call live-signal with trace_id= and the saved cursor; +if signals[] is non-empty, summarize and route any actionable comments; +if operator=human, note "user has control" and stop input actions; +if signals=null and operator=agent for 5 consecutive ticks, call +CronDelete and report "idle, loop stopped". +``` + +60s is the floor on Claude Code's scheduler. Stop the loop before Step 7 +(`/loop` stop, or whatever your harness exposes) so async ticks don't +interleave with the closing summary. + +**Idle-stop is the unhappy-path equivalent.** The trailing clause keeps a +forgotten loop from running all the way to its 7-day cron auto-expiry — if the +agent loses context, gets reassigned, or just forgets to stop the loop at Step +7, it self-terminates after ~5 quiet minutes (5 ticks × 60s) instead of polling +into the void. Pass the scheduled job id into `` at `/loop` time so the +prompt can call `CronDelete` against itself. Tune the threshold for the task: +5 is forgiving enough that brief AFK moments don't trigger it, short enough +that a forgotten loop doesn't burn tokens overnight. + +**Other harnesses.** Cursor, Codex, opencode, Gemini CLI, and Open SWE don't +expose an in-session scheduler equivalent to `/loop`. The in-context polling +discipline above is the supported path on those harnesses; the idle-gap case +isn't covered today and lands when the agent's next tool call fires. diff --git a/cli/skills/recipe-sightmap-setup/SKILL.md b/cli/skills/recipe-sightmap-setup/SKILL.md new file mode 100644 index 00000000..c11ad8cf --- /dev/null +++ b/cli/skills/recipe-sightmap-setup/SKILL.md @@ -0,0 +1,42 @@ +--- + +name: recipe-sightmap-setup +description: Short recipe to create sightmap definitions for a project from scratch. +metadata: + _generated_from: templates/skills/recipe-sightmap-setup/SKILL.md + requires: + skills: ["subtext:sightmap"] +--- +# Recipe: Sightmap Setup + +> **PREREQUISITE:** Read `subtext:sightmap` for the full schema reference. + +## Steps + +1. **Navigate to the page**: `subtext live view-navigate` or `subtext live view-new` +2. **Take a baseline snapshot**: `subtext live view-snapshot` to see the current a11y tree with generic roles +3. **Identify key UI components** in the snapshot (navigation, forms, cards, modals, etc.) +4. **Find good selectors** using `subtext live view-inspect` — this returns the full component tree with CSS selectors (tag, id, classes, `data-*` attributes, `aria-*`, `href`, etc.) on every node. Use it to identify stable targeting info, then switch back to `subtext live view-snapshot` for normal interaction. + Prefer `data-*` attributes when available — they're stable and semantically meaningful (e.g., `[data-component="ProductTile"]`, `[data-testid="checkout-button"]`). +5. **Create `.sightmap/components.yaml`** with component definitions (see `subtext:sightmap` skill for schema) +6. **Add memories** to key components — contextual notes that appear in a `[Guide]` section at the top of every snapshot. Focus on: + - **Auth/access**: passwords, test accounts, login flows (e.g., `"Password is 'argus'"`) + - **Stateful components**: how toggles, tabs, or modes change the UI (e.g., `"Audience toggle switches copy between builder/agent perspectives"`) + - **Forms**: required fields, validation rules, expected formats + - **Complex interactions**: multi-step flows, known quirks, non-obvious behavior + ```yaml + - name: LoginForm + selector: "[data-component='LoginForm']" + source: src/components/LoginForm.tsx + memory: + - "Test account: user@test.com / password123" + - "Shows CAPTCHA after 3 failed attempts" + ``` +7. **Upload the sightmap**: get the upload URL from `subtext live tunnel` or `subtext live connect`, then: + ```bash + subtext sightmap upload --url + ``` +8. **Take another snapshot** to verify component names appear (definitions are re-read on each snapshot) +9. **Add views** if the app has distinct routes — specify route patterns and view-scoped components +10. **Add requests** if key API endpoints should have semantic names — use `subtext live net-list` to identify them +11. **Verify enrichment**: take snapshots on different views, check `[View: ...]` headers, semantic names, and `[Guide]` section with memories all appear diff --git a/cli/skills/shared/SKILL.md b/cli/skills/shared/SKILL.md new file mode 100644 index 00000000..297fbabc --- /dev/null +++ b/cli/skills/shared/SKILL.md @@ -0,0 +1,56 @@ +--- + +name: shared +description: Foundation skill for the subtext plugin. MCP tool conventions, environment detection, security rules, and sightmap upload. +metadata: + _generated_from: templates/skills/shared/SKILL.md + +--- +# Shared + +Foundation for all subtext skills. Read this when any workflow or recipe lists it in PREREQUISITE. + +## Command Groups + +All commands ship in the `subtext` binary. Groups by subcommand: + +| Namespace | Commands | +|-----------|---------| +| `subtext live` | Browser automation: `subtext live connect`, `subtext live disconnect`, `subtext live view-*`, `subtext live act-*`, `subtext live log-*`, `subtext live net-*`, `subtext live tunnel`, `subtext live emulate`, `subtext live eval-script` | +| `subtext comment` | Comments: `subtext comment add`, `subtext comment list`, `subtext comment reply`, `subtext comment resolve` | +| `subtext doc` | Proof documents: `subtext doc create`, `subtext doc update`, `subtext doc attach`, `subtext doc close`, `subtext doc read`, `subtext doc diff`, `subtext doc list` | +| `subtext tunnel` | Reverse tunnel (built-in): `subtext tunnel connect`, `subtext tunnel disconnect`, `subtext tunnel status` | +| `subtext sightmap` | Sightmap: `subtext sightmap upload` | + +## Sightmap Upload + +Two commands return a sightmap upload URL: + +| Command | Field | Format | +|---------|-------|--------| +| `subtext live connect` | `sightmap_upload_url:` | text line in response | +| `subtext live tunnel` | `sightmapUploadUrl` | JSON field in response | + +If the project has `.sightmap/` definitions, upload them after getting the URL and **before** proceeding (before `subtext live view-new` for the tunnel-first flow): + +```bash +URL=$(subtext live tunnel --format json | jq -r .data.sightmapUploadUrl) +subtext sightmap upload --url "$URL" +``` + +The upload uses a single-use token embedded in the URL — no additional auth is needed. Do NOT pass the `sightmap` parameter directly to `subtext live connect`. + +## Discovering Parameters + +Run `subtext --help` to see parameters for any command. For example: + +```bash +subtext live connect --help +subtext comment add --help +``` + +## Security Rules + +- Never expose API tokens, session tokens, or credentials in output +- Confirm with the user before any write operation that modifies production data +- Session URLs may contain sensitive user data — don't log or repeat them unnecessarily diff --git a/cli/skills/sightmap/SKILL.md b/cli/skills/sightmap/SKILL.md new file mode 100644 index 00000000..98c5cc94 --- /dev/null +++ b/cli/skills/sightmap/SKILL.md @@ -0,0 +1,191 @@ +--- + +name: sightmap +description: Use when setting up the sight map (.sightmap/ YAML files) — defining components, views, requests, or other runtime semantics for the application. Also use when snapshot output shows generic a11y roles instead of meaningful names. +metadata: + _generated_from: templates/skills/sightmap/SKILL.md + +--- +# Sightmap + +## Why this exists + +`sitemap.xml` tells search engines how to crawl your site. `.sightmap/` **teaches** agents how to use it. + +A `.sightmap/` directory at the project root is a small set of YAML files that name your app's views, components, and API routes — checked in alongside your code, learned from the running app, and read by every coding agent that touches the repo. Each definition can carry a `memory:` list: freeform notes about quirks, invariants, and shortcuts the source code doesn't record. + +What you get: + +- Snapshots and network traces show **semantic names** (`NavBar`, `CheckoutForm`, `FetchFlights`) instead of generic a11y roles (`navigation`, `region`, `generic`). +- A `[Guide]` section at the top of every enriched snapshot surfaces the `memory:` entries on whatever components are visible — so the next agent picks up where the last one left off (auth gates, state quirks, validation rules). +- The artifact is a few small YAML files in your repo. It travels with the code, works in any agent (Claude, Cursor, Codex, anything that reads files), and is curated incrementally as agents work — not authored up-front. + +## Uploading definitions + +After obtaining a sightmap upload URL from `subtext live tunnel` or `subtext live connect`, upload with: + +```bash +subtext sightmap upload --url +``` + +`subtext sightmap upload` auto-discovers `.sightmap/` from the current working directory. Pass `--root ` to point at a different directory. + +The upload uses a single-use token embedded in the URL — no additional auth is needed. + +## What you define + +The `.sightmap` maps selectors, URL patterns, and API routes to semantic names that agents and analytics tools share across sessions. Three definition types: + +- **Components** — map CSS selectors to semantic names (e.g., `NavBar`, `SearchBox`) +- **Views** — map URL route patterns to screen names (e.g., `ProductDetail`, `UserSettings`) +- **Requests** — map API endpoints to semantic names with payload schemas (e.g., `FetchFlights`, `CreateOrder`) + +## File Location + +Place definition files anywhere under `.sightmap/` in the project root. All `*.yaml` and `*.yml` files are discovered recursively and merged. Organize however makes sense for your project: + +``` +.sightmap/ + components.yaml # global components (NavBar, Footer) + views.yaml # view definitions with scoped components + pages/ + search.yaml # components specific to search page + cart.yaml # components specific to cart page +``` + +All files use the same schema and can contain `components`, `views`, `requests`, or any combination. The directory structure is for human organization — at load time everything merges. + +## Components + +Components map CSS selectors to semantic names. They can be **global** (top-level `components` array) or **view-scoped** (nested inside a view definition). + +### Schema + +```yaml +version: 1 +components: + - name: NavBar + selector: "nav.main-navigation" + source: src/components/NavBar.tsx + description: Main site navigation with links and action buttons + children: + - name: nav-link + selector: "a.nav-link" + - name: nav-button + selector: "button.nav-btn" + + - name: ProductCard + selector: ".product-card" + source: src/components/ProductCard.tsx + description: Reusable product display (search results, recommendations, home page) + + - name: PromotedProduct + selector: ".product-card.promo" + source: src/components/ProductCard.tsx + description: Promoted/featured variant of ProductCard +``` + +### Fields + +- **version** (required): Must be `1` +- **components** (optional): Array of component definitions + - **name** (required): Semantic name shown in snapshots (replaces a11y role) + - **selector** (required): CSS selector to match elements. May be a string or a YAML list of strings for multiple alternatives (avoids ambiguity with commas in selectors). + - **source** (optional): Relative path to the source file implementing this component. Not uploaded to the server, but useful for agents navigating source code locally. + - **description** (optional): Brief description of the component's purpose. Not uploaded, but useful for agents reading the sightmap directly. + - **memory** (optional): List of contextual notes about this component, uploaded and shown in a Component Guide section of snapshot output. + - **children** (optional): Child components. Their selectors are scoped to the parent's subtree. + +### Multiple matches + +When multiple definitions match the same element, all names are shown. For example, a `.product-card.promo` element matches both `ProductCard` and `PromotedProduct`: + +``` +uid=1_20 ProductCard, PromotedProduct "Cool Shoes" visible interactive +``` + +### Selector tips + +- Prefer stable selectors: `data-` attributes, semantic class names, element roles +- Avoid fragile selectors: deeply nested paths, nth-child, generated class names +- Use a YAML list for multiple matching patterns: + ```yaml + selector: + - ".search-bar" + - "[role='search']" + ``` +- Children selectors are automatically scoped to parent subtree + +### Writing memory entries + +A memory entry should help the **next agent driving or reviewing the running app** understand what's on screen — *runtime behavior*, not source structure. Useful gut check: would this note show up usefully in the `[Guide]` of a snapshot the agent's about to interact with? If the answer requires holding the codebase in hand too, it belongs in source comments or `CLAUDE.md`, not here. + +**Good memory candidates:** stateful behavior (how toggles change the rendered UI), auth gates and credentials, form rules, multi-step interactions, runtime quirks. + +**Stay out of memory:** file paths, JSX/CSS patterns, style conventions, external doc references — all discoverable elsewhere or owned by other artifacts. + +## Views + +A view represents a screen or route in the application. Views provide: + +1. **"You are here" context** — the snapshot header identifies the current view by name +2. **Scoped component definitions** — components that only exist on certain views +3. **Metadata** — description, source file reference + +### Schema + +```yaml +version: 1 + +# Global components — matched on all views +components: + - name: NavBar + selector: "nav.main-nav" + +views: + - name: HomePage + route: "/" + source: pages/Home.tsx + components: + - name: HeroSection + selector: ".hero-section" + + - name: ProductDetail + route: "/products/*" + source: pages/ProductDetail.tsx + components: + - name: AddToCartButton + selector: "button.add-to-cart" +``` + +### View Fields + +- **name** (required): Semantic name shown in snapshot header +- **route** (required): Glob pattern matched against URL pathname +- **description** (optional): Brief description of the view +- **source** (optional): Relative path to the source file +- **components** (optional): View-scoped component definitions (additive model) + +### Route matching + +- `*` matches a single path segment; `**` matches any depth +- First matching view wins (definition order = priority) + +## Requests + +Requests map API endpoints to semantic names. See the MCP version of this skill for the full schema. The upload format is identical — `subtext sightmap upload` reads the same `.sightmap/` YAML files. + +## Enriched Snapshot Output + +With a matched view: + +``` +[View: ProductDetail "https://mystore.com/products/123"] + +uid=1_0 RootWebArea "Blue Widget - MyStore" + uid=1_1 NavBar visible interactive + uid=1_10 main visible + uid=1_20 AddToCartButton "Add to Cart" visible interactive +``` + +Without definitions, elements still get `visible`/`interactive` annotations but use generic a11y roles. diff --git a/cli/skills/tunnel/SKILL.md b/cli/skills/tunnel/SKILL.md new file mode 100644 index 00000000..7af9db32 --- /dev/null +++ b/cli/skills/tunnel/SKILL.md @@ -0,0 +1,128 @@ +--- + +name: tunnel +description: Use when opening a hosted browser connection against a localhost or local dev server URL. Sets up a reverse tunnel so the hosted browser can reach the user's local server. +metadata: + _generated_from: templates/skills/tunnel/SKILL.md + requires: + skills: ["subtext:shared", "subtext:live"] +--- +# Tunnel Setup for Hosted Browser + +When the hosted browser needs to load a page from the user's local dev server (e.g. `http://localhost:3000`), a reverse tunnel is required. The hosted browser cannot reach localhost directly — the tunnel proxies requests from the hosted infrastructure back to the user's machine. + +## Commands + +| Command | Description | +|---------|-------------| +| `subtext live tunnel` | Allocate a connection and get a relay URL for tunneling | +| `subtext tunnel connect` | Connect local server(s) to relay | +| `subtext tunnel status` | Check tunnel connection state | + +## When to Use + +- `subtext live connect` is called with a `localhost`, `127.0.0.1`, or other local URL +- The user asks to screenshot, test, or interact with their local dev server using hosted browser tools + +## The allowlist model + +`subtext tunnel connect` registers the tunnel with an **`allowedOrigins`** list. Every request that flows through the proxy is matched against the list; anything off-list is refused with a 502 (`ERR_TUNNEL_CONNECTION_FAILED` from chromium's perspective). This is the security boundary — without it, a buggy or hostile relay could probe arbitrary localhost services on the user's machine. + +**Grammar: `host:port`. No scheme. Subdomains are implicit.** + +- Each entry is a bare `host:port` — for example `example.test:8043` or `localhost:3000`. +- For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `example.test:8043` covers `app.example.test:8043`, `oauthtest.example.test:8043`, and so on. +- Hosts are restricted to the loopback class: `localhost`, `127.x`, `::1`, `*.test`, `*.localhost`. +- IP literals (`127.0.0.1:3000`, `[::1]:443`) match exactly with no subdomain expansion. +- Scheme is not part of the grammar; the same entry covers `http://` and `https://` on that `host:port`. + +The response from `subtext tunnel connect` may include a `canonicalized` field if your inputs were rewritten: + +```json +"canonicalized": [ + {"input": "www.example.test:8043", "canonical": "example.test:8043"} +] +``` + +Treat this as a soft warning: the relay accepted your entry but registered it as the canonical form. Use the canonical form in future calls. The parser also tolerates legacy `scheme://...` and `*.host:port` inputs for compatibility — both get canonicalized away. + +Default deny: omit something and chromium can't reach it through this tunnel. + +## Two Flows + +### Tunnel-first (recommended for localhost URLs) + +Set up the tunnel before opening a view. `subtext live tunnel` allocates the browser connection and returns a `connectionId` — use it with `subtext live view-new` to navigate. + +1. Run `subtext live tunnel` → returns `relayUrl`, `connectionId`, and `sightmapUploadUrl` +2. If the project has `.sightmap/` definitions, upload them now (see `subtext:shared`). Upload before `subtext live view-new` so the sightmap is active for the first snapshot. +3. Run `subtext tunnel connect` with `relayUrl` and `allowedOrigins` +4. Verify `state` is `"ready"` in the response +5. Run `subtext live view-new` with the `connection_id` from step 1 and the full localhost URL + +``` +subtext live tunnel → { relayUrl, connectionId: "abc-123", sightmapUploadUrl: "..." } +# upload .sightmap/ here if project has definitions (see subtext:shared) +subtext tunnel connect --relay-url --allowed-origins localhost:3000 +→ { state: "ready", tunnelId: "..." } +subtext live view-new --connection-id abc-123 --url http://localhost:3000/dashboard +``` + +### Connection-first (attach tunnel to existing connection) + +If `subtext live connect` was already called and you need to attach a tunnel afterward, pass the existing `connectionId` to `subtext live tunnel`. + +1. Run `subtext live tunnel` with `--connection-id` from the existing connection → returns `relayUrl` +2. Run `subtext tunnel connect` with `relayUrl` and `allowedOrigins` +3. Verify `state` is `"ready"` in the response +4. Navigate to the localhost URL with `subtext live view-navigate` + +## Picking an allowlist + +> **Default: list the trunk, not the subdomain you happen to be navigating to.** OAuth/SSO redirects will bounce out of any narrower entry within seconds of login, and chromium lands on `chrome-error://chromewebdata/` when that happens. The bare trunk implicitly covers every subdomain on the same port. + +- **App with auth/SSO redirects between subdomains** (the common case). List the trunk: + ``` + --allowed-origins example.test:8043 + ``` + This covers `app.example.test:8043`, `oauthtest.example.test:8043`, every other subdomain. Don't narrow to `app.example.test:8043` — the first OAuth bounce will fail. + +- **Multi-port local stack** (web app on `:3000` + API on `:4200`) — list each origin: + ``` + --allowed-origins localhost:3000,localhost:4200 + ``` + +- **Single-page local app, one origin, no auth** — bare trunk works: + ``` + --allowed-origins localhost:3000 + ``` + +## Diagnosing a chrome-error page + +Symptom: chromium lands on `chrome-error://chromewebdata/` (visible in `subtext live view-screenshot` or as a blank page after a navigation/click). + +Likely cause: an allowlist miss on a redirect — the navigation went somewhere not on `allowedOrigins` and the tunnel refused it. OAuth and SSO logins are the dominant trigger. + +Recovery (do this; don't keep navigating): + +1. `subtext tunnel disconnect` the current tunnel. +2. `subtext live tunnel` again — the `connection_id` is preserved across reconnect, so chromium continuity is fine. +3. `subtext tunnel connect` with a trunk that covers the redirect target (e.g. `example.test:8043` instead of `app.example.test:8043`). +4. Retry the navigation that failed. + +If the trunk reconnect still fails the same way, the navigation is going somewhere outside that trunk entirely (different domain, different port). Widen further or ask a human. + +## Common mistakes + +- **Don't use `subtext live connect` for localhost / local URLs.** It mints its own connection ID and can't bind to a tunnel — use the tunnel-first flow (`subtext live tunnel` → `subtext tunnel connect` → `subtext live view-new`) instead. +- **Don't narrow the allowlist to a specific subdomain.** Login flows redirect; the navigation target is rarely the only origin you'll need. Default to the trunk. +- **Don't include `https://` or `*.` in entries.** The parser strips them for compatibility, but the canonical form is just `host:port`. +- **Don't open multiple tunnels per connection.** A single tunnel carries many origins — widen the allowlist instead. + +## Notes + +- **Never fabricate a `connectionId`** — only use IDs returned from `subtext live connect`, `subtext live tunnel`, or `subtext tunnel connect` calls. +- `subtext live tunnel` allocates a browser connection on the same pod as the tunnel relay. In tunnel-first flow, this replaces `subtext live connect` — use `subtext live view-new` to open views instead. +- The tunnel stays connected across multiple views — you only need to set it up once per connection. +- If the tunnel disconnects (e.g. the relay restarts), it reconnects automatically. Run `subtext tunnel status` to check. +- The tunnel only needs to be set up for localhost/local URLs. Remote URLs (e.g. `https://example.com`) work directly without a tunnel. diff --git a/skills/comments/SKILL.md b/skills/comments/SKILL.md index 0f7926f7..e2cbcb0a 100644 --- a/skills/comments/SKILL.md +++ b/skills/comments/SKILL.md @@ -1,11 +1,14 @@ --- + name: comments description: Comment MCP tools for agent-user collaboration. Use when reviewing sessions or live pages to leave observations, read user feedback, reply, and resolve. metadata: + _generated_from: templates/skills/comments/SKILL.md requires: skills: ["subtext:shared"] --- + # Comments > **PREREQUISITE:** Read `subtext:shared` for MCP conventions and sightmap upload. diff --git a/skills/first-session/SKILL.md b/skills/first-session/SKILL.md index 9380208a..b61c2b0c 100644 --- a/skills/first-session/SKILL.md +++ b/skills/first-session/SKILL.md @@ -1,12 +1,15 @@ --- + name: first-session description: Agent explores the user's site via hosted broswer (live), leaving comments as it goes. Accepts a user-described flow or explores organically. Capped at ~10 interactions across 2-3 pages. Returns session URL, trace URL, and metrics. metadata: + _generated_from: templates/skills/first-session/SKILL.md requires: skills: ["subtext:shared", "subtext:live", "subtext:tunnel", "subtext:comments"] platform: claude-code --- + # First Session > **PREREQUISITE — Read inline before any other action:** Read skills `subtext:shared`, `subtext:live`, `subtext:tunnel`, `subtext:comments`. diff --git a/skills/live/SKILL.md b/skills/live/SKILL.md index 1a305323..46846dce 100644 --- a/skills/live/SKILL.md +++ b/skills/live/SKILL.md @@ -1,11 +1,14 @@ --- + name: live description: Live browser MCP tools for driving a hosted browser — connections, views, interactions, console, network, and tunnel. Use when reproducing flows, taking screenshots, or interacting with a running app. metadata: + _generated_from: templates/skills/live/SKILL.md requires: skills: ["subtext:shared"] --- + # Live Browser > **PREREQUISITE:** Read `subtext:shared` for MCP conventions and sightmap upload. diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md index c8717fe1..d2e017b6 100644 --- a/skills/onboard/SKILL.md +++ b/skills/onboard/SKILL.md @@ -1,12 +1,15 @@ --- + name: onboard description: Interactive first-run onboarding for new Subtext users. Connects to the user's local dev server, proves a small change with before/after evidence in a watchable trace, then bootstraps a starter sightmap from what was learned. metadata: + _generated_from: templates/skills/onboard/SKILL.md platform: claude-code requires: skills: ["subtext:shared", "subtext:proof", "subtext:sightmap", "subtext:live", "subtext:tunnel"] --- + # Onboarding > **PREREQUISITE — Read inline before any other action:** Read skills `subtext:proof`, `subtext:sightmap`, `subtext:live`, `subtext:tunnel`, `subtext:shared`. diff --git a/skills/proof/SKILL.md b/skills/proof/SKILL.md index 6bad9b57..c987a8ea 100644 --- a/skills/proof/SKILL.md +++ b/skills/proof/SKILL.md @@ -1,12 +1,15 @@ --- + name: proof description: You MUST use this skill when implementing, fixing, or refactoring code. Captures evidence artifacts (screenshots, network traces, code diffs, trace session links) into a proof document as you work. metadata: + _generated_from: templates/skills/proof/SKILL.md requires: skills: ["subtext:shared", "subtext:live", "subtext:comments", "subtext:docs"] mcp-server: subtext --- + # Proof > **PREREQUISITE — Read inline before any other action:** Read skills `subtext:shared`, `subtext:live`, `subtext:comments`, `subtext:docs`. @@ -19,33 +22,39 @@ Every code change that affects what a user sees must be visually proven. This sk **Always use `live-view-screenshot` with `upload: true`.** This uploads the screenshot to cloud storage and returns a signed URL you can attach to comments and PRs. + ``` live-view-screenshot({ connection_id, view_id, upload: true }) → { screenshot_url: "https://..." } ``` + **Do NOT use `artifact-upload` for screenshots.** It requires base64-encoding the entire PNG and frequently fails on large images. The `upload: true` flag on `live-view-screenshot` handles the upload server-side — smaller payload, no encoding issues. **Pass `screenshot_url` through verbatim — query string included.** The full signed URL is the credential. Don't strip `?Expires=…&GoogleAccessId=…&Signature=…` when copying it into PR descriptions, comments, or summaries — without those params GCS returns 403 and the image won't render. To attach a screenshot to a comment: + ``` live-view-screenshot({ ..., upload: true }) → screenshot_url comment-add({ ..., screenshot_url, intent: "looks-good", text: "AFTER: ..." }) ``` + ## Proof Document Every proof run creates a permanent record alongside the live session. This lets you — and any future reviewer — reconstruct exactly what changed, what it looked like before and after, and what evidence backed the decision to ship. **Create once, attach continuously, close at the end.** Pass the `verification` seed template (see `subtext:docs`) unless you have a better fit: + ``` doc-create(title: , content: ) → doc_id, doc_url ← save both ``` + The `doc_id` travels through every step below. The `doc_url` is the permanent link you hand to the user at the end. ## The Loop @@ -85,10 +94,12 @@ Drive the browser to the exact page/component/state where the change will be vis - Text: "BEFORE: [describe current state]. About to make [describe planned change]." - This is a chapter marker — it anchors the timeline for anyone reviewing the session later. 4. Attach to the proof document: + ``` doc-attach(doc_id, section: "Before", render_as: "image", url: {screenshot_url}, label: "Before: {description}") ``` + **Judgment call:** If the change affects multiple pages or states, capture BEFORE for each. ### Step 4: Make the code change @@ -98,12 +109,14 @@ Edit files, update components, fix styles — whatever the task requires. This is the only step where you leave the browser and work in the codebase. After editing, attach the diff to the proof document: + ``` doc-attach(doc_id, section: "Changes", render_as: "link", text: {git diff output}, content_type: "text/plain", label: {one-line description of what changed}) ``` + If a `live-act-*` tool returns `Control transferred to human viewer`, the reviewer has taken control. Enter standby — do not retry. Continue polling `live-signal`; when `operator` flips back to `agent`, resume UI-facing work. Backend changes that don't need visual verification can continue while you wait. ### Step 5: Verify the change with live tools @@ -117,10 +130,12 @@ Return to the browser. Refresh, hot reload, or reconnect if the dev server resta - Intent: `looks-good` if it matches intent, `bug` if something is wrong - Text: "AFTER: [describe what changed]. [Assessment against acceptance criteria]." 5. Attach to the proof document: + ``` doc-attach(doc_id, section: "After", render_as: "image", url: {screenshot_url}, label: "After: {description}") ``` + **Use live interaction tools to test the change:** - Click buttons, fill forms, hover elements — confirm the change works functionally, not just visually @@ -132,7 +147,10 @@ Return to the browser. Refresh, hot reload, or reconnect if the dev server resta If the AFTER doesn't match intent: 1. Call `live-view-screenshot` with `upload: true`, then `comment-add` with intent `bug` and the `screenshot_url`: "ISSUE: [what's wrong]. Fixing now." -2. Attach the issue screenshot to the proof document: `doc-attach(doc_id, section: "After", render_as: "image", url: {screenshot_url}, label: "Issue: {what's wrong}")` +2. Attach the issue screenshot to the proof document: + + `doc-attach(doc_id, section: "After", render_as: "image", url: {screenshot_url}, label: "Issue: {what's wrong}")` + 3. Go back to Step 4, make the fix 4. Return to Step 5, re-verify @@ -144,9 +162,15 @@ Once the change is verified: 1. Take a final `live-view-screenshot` with `upload: true` of the confirmed state 2. Call `comment-add` with intent `looks-good` and the `screenshot_url`: "VERIFIED: [summary of what was changed and confirmed]." -3. Attach the trace to the proof document: `doc-attach(doc_id, section: "Evidence", render_as: "link", url: {trace_url}, label: "Session trace")` -4. Re-read the document (`doc-read(doc_id)`) and confirm a cold reviewer would understand what changed, what was tested, and why. Attach anything missing. -5. Close: `doc-close(doc_id, status: "complete", summary: {one sentence outcome})` +3. Attach the trace to the proof document: + + `doc-attach(doc_id, section: "Evidence", render_as: "link", url: {trace_url}, label: "Session trace")` + +4. Re-read the document (`doc-read`) and confirm a cold reviewer would understand what changed, what was tested, and why. Attach anything missing. +5. Close: + + `doc-close(doc_id, status: "complete", summary: {one sentence outcome})` + 6. If a PR exists or will be created: - Include before/after screenshot URLs (from Step 3 and Step 5) in the PR description - Include the `trace_url` and `doc_url` so reviewers can watch the session and read the evidence record diff --git a/skills/recipe-sightmap-setup/SKILL.md b/skills/recipe-sightmap-setup/SKILL.md index ab125666..32dbe8f8 100644 --- a/skills/recipe-sightmap-setup/SKILL.md +++ b/skills/recipe-sightmap-setup/SKILL.md @@ -1,11 +1,14 @@ --- + name: recipe-sightmap-setup description: Short recipe to create sightmap definitions for a project from scratch. metadata: + _generated_from: templates/skills/recipe-sightmap-setup/SKILL.md requires: skills: ["subtext:sightmap"] --- + # Recipe: Sightmap Setup > **PREREQUISITE:** Read `subtext:sightmap` for the full schema reference. diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index c1dfcc6e..fff57aa2 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -1,12 +1,15 @@ --- + name: review description: Review a completed Subtext session and produce a structured summary. Use when you have a session URL and want to understand what happened — whether to verify another agent's proof work, walk through a dev / staging / preview flow, or summarize a captured session. Optionally emits reproduction steps on request (execution lives in `subtext:live`). Skip for tasks that modify code (use `subtext:proof`) or drive a running app (use `subtext:live`). metadata: + _generated_from: templates/skills/review/SKILL.md requires: skills: ["subtext:shared", "subtext:session", "subtext:comments"] mcp-server: subtext --- + # Review > **PREREQUISITE:** Read `subtext:shared`, `subtext:session`, `subtext:comments` for tool conventions. diff --git a/skills/session/SKILL.md b/skills/session/SKILL.md index 367c107c..72cc2b29 100644 --- a/skills/session/SKILL.md +++ b/skills/session/SKILL.md @@ -1,11 +1,14 @@ --- + name: session description: Session replay tools for analyzing Fullstory session recordings. Sparse API catalog — tools are self-describing. metadata: + _generated_from: templates/skills/session/SKILL.md requires: skills: ["subtext:shared"] --- + # Session Replay > **PREREQUISITE:** Read `subtext:shared` for MCP conventions and sightmap upload. diff --git a/skills/setup-plugin/SKILL.md b/skills/setup-plugin/SKILL.md index 54f9fe6c..db964826 100644 --- a/skills/setup-plugin/SKILL.md +++ b/skills/setup-plugin/SKILL.md @@ -1,11 +1,14 @@ --- + name: setup-plugin description: Install the Subtext plugin and configure MCP servers. Authenticates via OAuth or API Key. metadata: + _generated_from: templates/skills/setup-plugin/SKILL.md requires: skills: ["subtext:shared"] --- + # Setup Plugin Install and verify the Subtext plugin/extension. Works for Claude Code, Cursor, Codex, and Gemini CLI. diff --git a/skills/shared/SKILL.md b/skills/shared/SKILL.md index 215735c5..5e8ce80a 100644 --- a/skills/shared/SKILL.md +++ b/skills/shared/SKILL.md @@ -1,8 +1,13 @@ --- + name: shared description: Foundation skill for the subtext plugin. MCP tool conventions, environment detection, security rules, and sightmap upload. +metadata: + _generated_from: templates/skills/shared/SKILL.md + --- + # Shared Foundation for all subtext skills. Read this when any workflow or recipe lists it in PREREQUISITE. diff --git a/skills/sightmap/SKILL.md b/skills/sightmap/SKILL.md index dcad68f7..b5221c4a 100644 --- a/skills/sightmap/SKILL.md +++ b/skills/sightmap/SKILL.md @@ -1,8 +1,13 @@ --- + name: sightmap description: Use when setting up the sight map (.sightmap/ YAML files) — defining components, views, requests, or other runtime semantics for the application. Also use when snapshot output shows generic a11y roles instead of meaningful names. +metadata: + _generated_from: templates/skills/sightmap/SKILL.md + --- + # Sightmap ## Why this exists diff --git a/skills/tunnel/SKILL.md b/skills/tunnel/SKILL.md index 6ac3c870..d43efc13 100644 --- a/skills/tunnel/SKILL.md +++ b/skills/tunnel/SKILL.md @@ -1,11 +1,14 @@ --- + name: tunnel description: Use when opening a hosted browser connection against a localhost or local dev server URL. Sets up a reverse tunnel so the hosted browser can reach the user's local server. metadata: + _generated_from: templates/skills/tunnel/SKILL.md requires: skills: ["subtext:shared", "subtext:live"] --- + # Tunnel Setup for Hosted Browser > **ENVIRONMENT:** If a `subtext-environment` skill is available in the host project, read it before connecting — it specifies which MCP server prefix to use for live and tunnel tools. diff --git a/skills/using-subtext/SKILL.md b/skills/using-subtext/SKILL.md index 61d550e9..3f1bebad 100644 --- a/skills/using-subtext/SKILL.md +++ b/skills/using-subtext/SKILL.md @@ -1,8 +1,13 @@ --- + name: using-subtext description: Use when starting any conversation that may involve rendered UI, observed sessions, or producing reviewer-facing evidence (screenshots, viewer links, code diffs, command output). Establishes how subtext skills compose and when to invoke them before any response or action. +metadata: + _generated_from: templates/skills/using-subtext/SKILL.md + --- + If the task touches rendered UI, observed sessions, or producing proof-of-work evidence, you MUST invoke the relevant subtext skill diff --git a/skills/README.md b/templates/skills/README.md similarity index 100% rename from skills/README.md rename to templates/skills/README.md diff --git a/skills/authoring.md b/templates/skills/authoring.md similarity index 100% rename from skills/authoring.md rename to templates/skills/authoring.md diff --git a/templates/skills/comments/SKILL.md b/templates/skills/comments/SKILL.md new file mode 100644 index 00000000..f5b474c5 --- /dev/null +++ b/templates/skills/comments/SKILL.md @@ -0,0 +1,102 @@ +--- +name: comments +description: Comment MCP tools for agent-user collaboration. Use when reviewing sessions or live pages to leave observations, read user feedback, reply, and resolve. +metadata: + targets: [mcp, cli] + requires: + skills: ["subtext:shared"] +--- + +# Comments + +> **PREREQUISITE:** Read `subtext:shared` for {{if ne .Target "cli"}}MCP {{end}}conventions and sightmap upload. + +Tool catalog and judgment rules for comment-based agent-user collaboration. {{if ne .Target "cli"}}Comment tools are available on the subtext MCP server.{{else}}Comment commands are available in the `subtext` CLI.{{end}} + +{{if eq .Target "cli"}}## Commands{{else}}## MCP Tools{{end}} + +| Tool | Description | +|------|-------------| +| {{tool "comment-list"}} | Read all comments/annotations on a trace, with thread structure | +| {{tool "comment-add"}} | Leave a comment on a trace, optionally tied to a page and timestamp | +| {{tool "comment-reply"}} | Reply to an existing comment by ID | +| {{tool "comment-resolve"}} | Mark a comment thread as resolved | + +All comment tools are **stateless** — they identify the parent trace by `trace_id` (preferred) or `session_id` (deprecated; in `deviceId:sessionId` format), rather than requiring an active connection. + +### `trace_id` vs `session_id` + +Comments hang off a **trace** — the durable parent identifier that survives even when no FullStory session was captured. Every tool that needs a parent accepts either: + +- `trace_id` — the 12-char base62 id you get from {{tool "live-connect"}} (`trace_id:` line, or parse the trailing path of `trace_url`){{if ne .Target "cli"}} and from {{tool "review-open"}} (`trace_id:` line in the response){{end}}. **Prefer this.** It's stable, works for traces with no underlying FS session, and is the only key the storage layer actually uses. +- `session_id` — the legacy `deviceId:sessionId` form. Still accepted for callers that only have an FS session URL on hand. The server promotes it to a trace_id under the hood. Responses include a one-line deprecation hint when you use this path. + +{{tool "comment-resolve"}} only needs `comment_id`; the parent is looked up server-side. + +## Discovering Parameters + +Parameter schemas are visible in the tool definition at call time. + +## Screenshots + +Comment tools do **not** auto-capture screenshots. To attach a screenshot, pass a `screenshot_url` to {{tool "comment-add"}}. This URL must point to a pre-captured screenshot (e.g., from {{tool "live-view-screenshot"}} or another source). + +> **Note:** To attach a screenshot, first capture one via {{tool "live-view-screenshot"}}{{if ne .Target "cli"}} or {{tool "review-view"}}{{end}}, then pass the returned URL as `screenshot_url` **verbatim** — the signed query string (`?Expires=…&GoogleAccessId=…&Signature=…`) is the credential. Stripping it returns 403 from GCS and the image won't render. + +When the comment is about a specific element, capture a focused clip by passing `component_id` (and a small `expand_pct` for context) to the screenshot tool. A focused clip is far more useful in a comment than a full viewport — the reader sees exactly what you're pointing at. + +## Markdown + +Comment text is rendered as **Markdown** in the comment thread UI. Use standard formatting freely: + +- **Bold** / *italic* for emphasis +- Bulleted and numbered lists for structured observations +- `code spans` and fenced code blocks for selectors, error messages, or snippets +- [Links](url) to reference external evidence or docs + +Keep formatting proportional to the comment — a one-line observation doesn't need a bulleted list. + +## Intents + +When adding a comment, classify it: + +| Intent | Use when | +|--------|----------| +| `bug` | Something is broken | +| `tweak` | Minor improvement needed | +| `ask` | Question for user or another agent | +| `looks-good` | Confirming an area passes review | + +## Rules + +1. **List before acting.** Every time you receive a session URL or page URL, {{tool "comment-list"}} first. Never assume you know what feedback exists. +2. **Reply before resolving.** The reply is the audit trail. A silent resolve is invisible history. +3. **Agents resolve their own observations freely** after visual verification confirms the fix. +4. **User comments: reply with status, let users resolve** — unless they explicitly say "resolve it" or you have screenshot proof the specific issue is gone. +5. **Agent-to-agent handoffs:** Read prior agent's comments via {{tool "comment-list"}}, reply to acknowledge before starting your own work, don't resolve another agent's comments without verifying. + +## Review Handoff Loop + +Comments enable asynchronous review between agents and users: + +1. Agent does work → runs visual verification +2. Agent calls {{tool "comment-add"}} with observations (`bug`, `tweak`, `looks-good`) +3. Agent shares the trace URL with the user +4. User reviews → reads agent comments → leaves own comments/replies +5. User shares URL back to agent +6. Agent calls {{tool "comment-list"}} to read ALL feedback +7. Agent addresses each issue → {{tool "comment-reply"}} with status +8. Agent calls {{tool "comment-resolve"}} ONLY on verified fixes +9. Repeat from step 4 until user is satisfied + +## Gotchas + +- Forgetting to {{tool "comment-list"}} on entry — you'll duplicate work or miss user feedback +- Resolving user comments without replying — no audit trail, user doesn't know what happened +- Resolving without visual verification — "I fixed it" without a screenshot is a claim, not evidence +- Adding comments without navigating to the relevant page first — comments attach to what's currently visible + +## See Also + +- `subtext:shared` — MCP conventions and sightmap upload +{{if ne .Target "cli"}}- `subtext:session` — session replay tools (review-*){{end}} diff --git a/skills/docs/SKILL.md b/templates/skills/docs/SKILL.md similarity index 72% rename from skills/docs/SKILL.md rename to templates/skills/docs/SKILL.md index 5bdaaf5a..3124e768 100644 --- a/skills/docs/SKILL.md +++ b/templates/skills/docs/SKILL.md @@ -2,29 +2,30 @@ name: docs description: Proof document MCP tools for creating, updating, and closing agent work documentation. Use when tracking a bug fix, UX review, or changeset to produce a permanent, evidence-backed record. metadata: + targets: [mcp, cli] requires: skills: ["subtext:shared"] --- # Docs -> **PREREQUISITE:** Read `subtext:shared` for MCP conventions. +> **PREREQUISITE:** Read `subtext:shared` for {{if ne .Target "cli"}}MCP {{end}}conventions. -Tool catalog and judgment rules for agent-produced proof documents. Doc tools are available on the subtext MCP server. +Tool catalog and judgment rules for agent-produced proof documents. {{if ne .Target "cli"}}Doc tools are available on the subtext MCP server.{{else}}Doc commands are available in the `subtext` CLI.{{end}} The document tools are intentionally generic: the server does not know about bug-fix vs. ux-review vs. verification workflows. Pass your preferred structure via `doc-create(content: ...)`; this skill ships opinionated templates below. -## MCP Tools +{{if eq .Target "cli"}}## Commands{{else}}## MCP Tools{{end}} | Tool | Description | |------|-------------| -| `doc-create` | Open a new proof document with a title and optional seed markdown | -| `doc-update` | Edit the document: replace text, append content, or update metadata | -| `doc-attach` | Attach evidence (screenshot, replay, log, diff, report) into a named section | -| `doc-close` | Finalize the document, write a permanent snapshot, get a stable URL | -| `doc-read` | Read the current or a past version of a document | -| `doc-diff` | Diff two document versions | -| `doc-list` | List open or closed documents, optionally filtered by tag, ref, trace_id, or status | +| {{tool "doc-create"}} | Open a new proof document with a title and optional seed markdown | +| {{tool "doc-update"}} | Edit the document: replace text, append content, or update metadata | +| {{tool "doc-attach"}} | Attach evidence (screenshot, replay, log, diff, report) into a named section | +| {{tool "doc-close"}} | Finalize the document, write a permanent snapshot, get a stable URL | +| {{tool "doc-read"}} | Read the current or a past version of a document | +| {{tool "doc-diff"}} | Diff two document versions | +| {{tool "doc-list"}} | List open or closed documents, optionally filtered by tag, ref, trace_id, or status | ## Lifecycle @@ -32,15 +33,15 @@ The document tools are intentionally generic: the server does not know about bug doc-create → [doc-update / doc-attach]* → doc-close ``` -- `doc-create` writes the title, auto-managed metadata line, and (if `content` is provided) your seed markdown. Without `content`, it creates a document with a single empty `## Evidence` section. -- `doc-update` and `doc-attach` fill evidence during work. -- `doc-close` writes an immutable version snapshot (`v1.md`, `v2.md`, …) and marks the doc `complete`, `partial`, or `abandoned`. +- {{tool "doc-create"}} writes the title, auto-managed metadata line, and (if `content` is provided) your seed markdown. Without `content`, it creates a document with a single empty `## Evidence` section. +- {{tool "doc-update"}} and {{tool "doc-attach"}} fill evidence during work. +- {{tool "doc-close"}} writes an immutable version snapshot (`v1.md`, `v2.md`, …) and marks the doc `complete`, `partial`, or `abandoned`. - Open docs auto-close as `abandoned` after 24h of inactivity. -- A closed doc can be reopened by calling `doc-update` on it. The metadata header will switch to `Latest closed: vN | Draft in progress` until the next `doc-close` bumps to `v{N+1}`. +- A closed doc can be reopened by calling {{tool "doc-update"}} on it. The metadata header will switch to `Latest closed: vN | Draft in progress` until the next {{tool "doc-close"}} bumps to `v{N+1}`. ## Seed templates -Pass one of these as `content` on `doc-create` to shape the document up front. Agents may edit headings and add sections freely afterwards via `doc-update`. +Pass one of these as `content` on {{tool "doc-create"}} to shape the document up front. Agents may edit headings and add sections freely afterwards via {{tool "doc-update"}}. ### bug-fix @@ -132,13 +133,13 @@ Pass one of these as `content` on `doc-create` to shape the document up front. A ## Attaching evidence -`doc-attach` has four source modes. Provide exactly one: +{{tool "doc-attach"}} has four source modes. Provide exactly one: | Mode | Use when | Params | |------|----------|--------| | `base64_data` + `content_type` | Binary content (images, PDF) generated in-session | `label`, `section`, `render_as` | | `text` + `content_type` | Plain-text content (markdown plans, logs, JSON). Avoids base64 inflation. | `label`, `section`, `render_as` | -| `artifact_id` | Referencing a file from a previous `artifact-upload` | `label`, `section`, `render_as`, optionally `artifact_ext` | +| `artifact_id` | Referencing a file from a previous {{tool "artifact-upload"}} | `label`, `section`, `render_as`, optionally `artifact_ext` | | `url` | External URL (session replay, viewer link, Grafana, Loom) | `label`, `section`, `render_as` | Additional params: @@ -172,16 +173,16 @@ Attach at every capture point. Typical patterns: When work is done and evidence is captured, call `doc-close(status: "complete", summary: ...)`. If the work was incomplete, close as `partial` and explain in `summary`. If you never finished, `abandoned`. -There is no server-side score. Before closing, re-read the document (`doc-read`) and ask whether a human reviewer opening the URL cold would understand what changed, what was tested, and why. If not, attach what's missing. +There is no server-side score. Before closing, re-read the document ({{tool "doc-read"}}) and ask whether a human reviewer opening the URL cold would understand what changed, what was tested, and why. If not, attach what's missing. ## Rules 1. **Create at entry, not end.** A doc started after the work captures nothing useful. -2. **Seed structure up front.** Pass a `content` template on `doc-create`. Editing after-the-fact is harder than starting with the right shape. +2. **Seed structure up front.** Pass a `content` template on {{tool "doc-create"}}. Editing after-the-fact is harder than starting with the right shape. 3. **Pass `doc_id` to subagents.** Evidence from subagents belongs in the same document. -4. **Fill sections progressively.** Don't batch all `doc-update` calls at the end. +4. **Fill sections progressively.** Don't batch all {{tool "doc-update"}} calls at the end. 5. **Prefer `text` over `base64_data`** for textual evidence (markdown, logs, JSON). It avoids token inflation. -6. **Close with a useful summary.** The summary appears in `doc-list` and the permanent snapshot. +6. **Close with a useful summary.** The summary appears in {{tool "doc-list"}} and the permanent snapshot. 7. **Share the `doc_url`.** Give it to the user when closing so they have the permanent link. ## End-to-end transcript (bug fix) @@ -235,8 +236,8 @@ User receives: "Fix complete. Proof document: https://..." ## Gotchas -- Forgetting to `doc-create` at entry — you'll have no document to attach evidence to -- Seeding without `content` and then painting structure with `doc-update` — slower and more error-prone than seeding up front +- Forgetting to {{tool "doc-create"}} at entry — you'll have no document to attach evidence to +- Seeding without `content` and then painting structure with {{tool "doc-update"}} — slower and more error-prone than seeding up front - Using `base64_data` for text — use `text` instead to avoid ~33% inflation and wasted tokens - Not sharing the `doc_url` — the user can't find the proof document without it diff --git a/templates/skills/first-session/SKILL.md b/templates/skills/first-session/SKILL.md new file mode 100644 index 00000000..f04b97d7 --- /dev/null +++ b/templates/skills/first-session/SKILL.md @@ -0,0 +1,76 @@ +--- +name: first-session +description: Agent explores the user's site via hosted broswer (live), leaving comments as it goes. Accepts a user-described flow or explores organically. Capped at ~10 interactions across 2-3 pages. Returns session URL, trace URL, and metrics. +metadata: + targets: [mcp] + requires: + skills: ["subtext:shared", "subtext:live", "subtext:tunnel", "subtext:comments"] + platform: claude-code +--- + +# First Session + +> **PREREQUISITE — Read inline before any other action:** Read skills `subtext:shared`, `subtext:live`, `subtext:tunnel`, `subtext:comments`. + +Explore the user's site via hosted browser tools, leaving comments as a breadcrumb trail of agent reasoning. + +## Pre-check + +If the user already has a session URL (they mention one or it was passed in), skip exploration and return it directly. + +## Prerequisites + +Before starting, confirm: +1. **Dev server is running** — the user must have their app running locally (or provide a deployed URL) +2. **Tunnel if localhost** — use the tunnel-first flow: `live-tunnel` → `tunnel-connect` → `live-view-new` (see `subtext:tunnel`). Do **not** use `live-connect` for localhost URLs. + +## Input + +The orchestrator provides: +- **App URL** (e.g., `http://localhost:3000`) +- **Flow description** — either a user-provided goal (e.g., "sign up and explore the dashboard") or `"feeling lucky"` for organic exploration + +## Exploration Loop + +``` +1. Connect to app URL (live-connect for remote, or tunnel-first flow for localhost — see Prerequisites) +2. live-view-snapshot to see the page +3. Optionally comment-add with an observation about the page or what just happened, if noteworthy +4. Choose an interaction: + - If flow described: work toward that goal + - If "feeling lucky": infer from the site's purpose what a real user would do + (e.g., e-commerce → browse and add to cart; SaaS → sign up and explore) +5. Perform the interaction (live-act-click, live-act-fill, live-view-navigate) +6. Repeat from step 2 until ~10 interactions +7. live-disconnect → capture session URL +``` + +### Choosing interactions ("feeling lucky") + +Read the page, understand the site's purpose from its content and navigation, and act like a curious first-time user would. No rigid strategy — be organic: +- Follow primary CTAs and navigation +- Try forms if they look interesting +- Explore different sections of the site +- If something looks interactive, try it + +### Interaction cap + +Stop after **~10 interactions** (clicks, fills, navigations) across **2-3 pages**. This produces a session that's substantial enough to review without dragging on. If at any point you get stuck trying to get past a localhost issue (e.g. login wall, build issue, etc) - don't waste time figuring it out, just pop out and ask the user for help to continue. + +## Comments + +Use `comment-add` (from `subtext:comments`) to leave observations throughout exploration. Comments attach to the session and appear in the viewer sidebar. + +### Comment guidelines + +- **Comment when noteworthy** — something unexpected happened, UI was confusing, a form behaved oddly, or a page stood out. Don't comment on routine clicks or straightforward navigation. +- **Be specific about confusion** — when something is hard to figure out, describe what made it difficult. These observations become sightmap memory candidates later. +- Use the `bug` intent for issues found, `ask` for ambiguous UI, `looks-good` for smooth flows + +## Output + +Return to the orchestrator: +- **Trace URL** — the `trace_url` from the connect step (`live-connect` for remote URLs, `live-view-new` for tunnel-first). Print to the user so they can watch live. +- **Session URL** from the hosted browser handshake +- **Total interaction count** +- Subagent usage stats (tokens, duration) are captured automatically by the orchestrator diff --git a/templates/skills/live/SKILL.cli.md b/templates/skills/live/SKILL.cli.md new file mode 100644 index 00000000..f513e437 --- /dev/null +++ b/templates/skills/live/SKILL.cli.md @@ -0,0 +1,157 @@ +# Live Browser + +> **PREREQUISITE:** Read `subtext:shared` for conventions and sightmap upload. + +Command catalog for live browser commands (`subtext live *`). These commands let you open browser connections, navigate views, interact with elements, and inspect console/network activity. + +## Commands + +### Connections + +| Command | Description | +|---------|-------------| +| {{tool "live-connect"}} | Open a browser connection to a URL. Returns screenshot, component tree, `fs_session_url`, `trace_id`, `trace_url`, and `capture_status`. | +| {{tool "live-disconnect"}} | Close a browser connection. Returns `fs_session_url`, `trace_id`, and `trace_url`. | +| {{tool "live-emulate"}} | Set device emulation (viewport, user agent, etc.) | + +### Views + +| Command | Description | +|---------|-------------| +| {{tool "live-view-navigate"}} | Navigate the current view to a new URL | +| {{tool "live-view-new"}} | Open a new view (tab) | +| {{tool "live-view-list"}} | List all open views | +| {{tool "live-view-select"}} | Switch to a different view | +| {{tool "live-view-close"}} | Close a view | +| {{tool "live-view-snapshot"}} | Component tree snapshot (no screenshot) | +| {{tool "live-view-inspect"}} | Component tree with full CSS selectors — for sightmap authoring only, not general use | +| {{tool "live-view-screenshot"}} | Visual screenshot of current view. Pass `component_id` to clip to a specific element's bounding box; optional `expand_pct` (0–100) grows the clip rect outward for surrounding context, clamped to the viewport. | +| {{tool "live-view-resize"}} | Resize the viewport | + +### Interactions + +| Command | Description | +|---------|-------------| +| {{tool "live-act-click"}} | Click an element by UID | +| {{tool "live-act-hover"}} | Hover over an element | +| {{tool "live-act-fill"}} | Fill a text input | +| {{tool "live-act-keypress"}} | Press a key or key combination | +| {{tool "live-act-drag"}} | Drag from one element to another | +| {{tool "live-act-scroll"}} | Scroll the view: by component UID (into view), pixel delta, or absolute position | +| {{tool "live-act-wait-for"}} | Wait for a condition (selector, navigation, timeout) | +| {{tool "live-act-dialog"}} | Accept or dismiss a browser dialog | +| {{tool "live-act-upload"}} | Upload a file to a file input | + +### Developer Tools + +| Command | Description | +|---------|-------------| +| {{tool "live-eval-script"}} | Run JavaScript in the page context | +| {{tool "live-log-list"}} | List console messages | +| {{tool "live-log-get"}} | Get details of a specific console message | +| {{tool "live-net-list"}} | List network requests | +| {{tool "live-net-get"}} | Get details of a specific network request | + +### Signals + +| Command | Description | +|---------|-------------| +| {{tool "live-signal"}} | Poll the trace for operator state and new comment signals. Returns `{operator, operator_email?, signals[], cursor, server_time}`. Cursor-based — pass `since` back on the next call. | + +### Tunnel + +| Command | Description | +|---------|-------------| +| {{tool "live-tunnel"}} | Get tunnel relay URL for connecting to localhost | + +## Discovering Parameters + +Run `subtext live --help` to see parameters for any command. + +## Trace and Session URLs + +`fs_session_url`, `trace_id`, and `trace_url` are returned by {{tool "live-connect"}}, {{tool "live-disconnect"}}, {{tool "live-view-navigate"}}, {{tool "live-view-new"}}, and {{tool "live-view-snapshot"}}. + +- **fs_session_url** — the raw Fullstory session URL. +- **trace_id** — the 12-char base62 id for this connection's trace. **Capture and reuse this** as the parent identifier for `subtext comment *` commands. The trailing path segment of `trace_url` is the same id. +- **trace_url** — a shareable link that opens the live viewer in a browser. **Always print this to the user** so they can watch the agent's browser in real time. + +After every connection is established — via {{tool "live-connect"}} or {{tool "live-view-new"}} (tunnel-first flow) — output the URL on its own line: + +``` +Viewer: {trace_url} +``` + +## Capture Status + +Every live command response that touches a view includes a `capture_status` field — +this includes {{tool "live-connect"}}, {{tool "live-view-new"}}, {{tool "live-view-navigate"}}, +{{tool "live-view-snapshot"}}, and {{tool "live-view-screenshot"}}. Check it after **every** such +call (not just {{tool "live-connect"}}) and respond as follows: + +- `active`: proceed normally. +- `blocked`: tell the user to check capture quota and verify the target domain is allow listed in Subtext data capture settings. +- `snippet_not_found` or `api_unavailable`: tell the user something went wrong during setup and they should check their API key and endpoint configuration. +- any other status: something went wrong, try again + +This matters especially in the **tunnel-first flow**, where {{tool "live-connect"}} is +never called — it's easy to miss the status if you assume the check only applies +to that one tool. + +## Operator and Signals + +{{tool "live-signal"}} is the trace's read channel for human-side activity. Call it +between action loops to learn about new comments and the operator state. + +**Response shape:** + +```json +{ + "operator": "agent" | "human", + "operator_email": "...", // present when operator=human + "signals": [ + { + "type": "comment", "id": "...", "ts": "...", + "text": "...", "author_type": "user|agent", + "intent": "...", "resolved": false, "parent_id": "..." + } + ], + "cursor": "...", // round-trip back as `since` next call + "server_time": "..." +} +``` + +**Polling pattern.** + +1. First call after {{tool "live-connect"}}: omit `since` to baseline the cursor. +2. Subsequent calls: pass the previous response's `cursor` as `since`. Only signals newer than the cursor come back. +3. Comment signals carry full text and metadata inline — no follow-up `subtext comment list` is needed for the new ones. +4. Save the new `cursor` after every call. + +**Operator gate.** When `operator=human`, the user has taken browser control. The `live-act-*` input commands (click, fill, hover, keypress, drag, dialog, upload) return a structured error and **must not be retried**. Read-only commands ({{tool "live-view-snapshot"}}, {{tool "live-view-screenshot"}}, `live-log-*`, `live-net-*`, {{tool "live-signal"}}) keep working. Stay read-only and keep polling — when `operator` flips back to `agent`, resume normal work. + +{{tool "live-act-wait-for"}} is excluded from the gate (observation-only). + +## Tips + +- **Default to {{tool "live-view-snapshot"}} for all page observation.** It carries sightmap context (component names, view names, `[src: ...]` annotations) and provides the component UIDs needed by `act-*` commands. Always call it before any interaction sequence. +- **Use {{tool "live-view-screenshot"}} only for visual evidence** — before/after comparisons, layout debugging, or screenshots to embed in PRs. The command returns the image inline by default so you can verify framing. Pass `--upload` only when you need a hosted URL for a PR or comment attachment. +- When the screenshot is evidence about a specific element, clip to it with `--component-id` (and small `--expand-pct` for context). `expand_pct` caps at 100, so very short elements (a label, a textbox) still produce thin slices — clip to a wider parent in that case. +- {{tool "live-view-inspect"}} is for **sightmap authoring only** — it returns verbose CSS selectors on every node. Do not use it as a general snapshot replacement. Use it once to discover selectors, write your `.sightmap/` YAML, then use {{tool "live-view-snapshot"}} for everything else. +- Component names from sightmap appear in snapshots — use `[src: ...]` annotations to find source files. +- Close connections when done to free server resources. + +## Tunnel Setup + +When the hosted browser needs to reach `localhost` or local dev URLs, use the tunnel-first flow: + +1. Run {{tool "live-tunnel"}} — allocates a browser connection and returns `relayUrl` + `connectionId` +2. Run {{tool "tunnel-connect"}} with `relayUrl` and `allowedOrigins` (one or more local origins the tunnel may serve) +3. Run {{tool "live-view-new"}} with the `connection_id` and localhost URL + +Do **not** use {{tool "live-connect"}} for localhost URLs — it mints its own connection ID and can't bind to the tunnel. See `subtext:tunnel` for full details, including the trunk pattern (`host:port` covers all subdomains on the same port) needed when local apps redirect across subdomains (e.g. OAuth flows). + +## See Also + +- `subtext:shared` — shared conventions and sightmap upload +- `subtext:tunnel` — Reverse tunnel setup for localhost access diff --git a/templates/skills/live/SKILL.md b/templates/skills/live/SKILL.md new file mode 100644 index 00000000..c6eea36d --- /dev/null +++ b/templates/skills/live/SKILL.md @@ -0,0 +1,167 @@ +--- +name: live +description: Live browser MCP tools for driving a hosted browser — connections, views, interactions, console, network, and tunnel. Use when reproducing flows, taking screenshots, or interacting with a running app. +metadata: + targets: [mcp, cli] + requires: + skills: ["subtext:shared"] +--- + +# Live Browser + +> **PREREQUISITE:** Read `subtext:shared` for MCP conventions and sightmap upload. +> **ENVIRONMENT:** If a `subtext-environment` skill is available in the host project, read it before connecting — it specifies which MCP server prefix to use for live tools. + +API catalog for live browser tools (all prefixed `live-`) on the unified subtext MCP server. These tools let you open browser connections, navigate views, interact with elements, and inspect console/network activity. + +{{if eq .Target "cli"}}## Commands{{else}}## MCP Tools{{end}} + +### Connections + +| Tool | Description | +|------|-------------| +| {{tool "live-connect"}} | Open a browser connection to a URL. Returns screenshot, component tree, `fs_session_url`, `trace_id`, `trace_url`, and `capture_status`. | +| {{tool "live-disconnect"}} | Close a browser connection. Returns `fs_session_url`, `trace_id`, and `trace_url`. | +| {{tool "live-emulate"}} | Set device emulation (viewport, user agent, etc.) | + +### Views + +| Tool | Description | +|------|-------------| +| {{tool "live-view-navigate"}} | Navigate the current view to a new URL | +| {{tool "live-view-new"}} | Open a new view (tab) | +| {{tool "live-view-list"}} | List all open views | +| {{tool "live-view-select"}} | Switch to a different view | +| {{tool "live-view-close"}} | Close a view | +| {{tool "live-view-snapshot"}} | Component tree snapshot (no screenshot) | +| {{tool "live-view-inspect"}} | Component tree with full CSS selectors — for sightmap authoring only, not general use | +| {{tool "live-view-screenshot"}} | Visual screenshot of current view. Pass `component_id` to clip to a specific element's bounding box; optional `expand_pct` (0–100) grows the clip rect outward for surrounding context, clamped to the viewport. | +| {{tool "live-view-resize"}} | Resize the viewport | + +### Interactions + +| Tool | Description | +|------|-------------| +| {{tool "live-act-click"}} | Click an element by UID | +| {{tool "live-act-hover"}} | Hover over an element | +| {{tool "live-act-fill"}} | Fill a text input | +| {{tool "live-act-keypress"}} | Press a key or key combination | +| {{tool "live-act-drag"}} | Drag from one element to another | +| {{tool "live-act-scroll"}} | Scroll the view: by component UID (into view), pixel delta, or absolute position | +| {{tool "live-act-wait-for"}} | Wait for a condition (selector, navigation, timeout) | +| {{tool "live-act-dialog"}} | Accept or dismiss a browser dialog | +| {{tool "live-act-upload"}} | Upload a file to a file input | + +### Developer Tools + +| Tool | Description | +|------|-------------| +| {{tool "live-eval-script"}} | Run JavaScript in the page context | +| {{tool "live-log-list"}} | List console messages | +| {{tool "live-log-get"}} | Get details of a specific console message | +| {{tool "live-net-list"}} | List network requests | +| {{tool "live-net-get"}} | Get details of a specific network request | + +### Signals + +| Tool | Description | +|------|-------------| +| {{tool "live-signal"}} | Poll the trace for operator state and new comment signals. Returns `{operator, operator_email?, signals[], cursor, server_time}`. Cursor-based — pass `since` back on the next call. | + +### Tunnel + +| Tool | Description | +|------|-------------| +| {{tool "live-tunnel"}} | Get tunnel relay URL for connecting to localhost | + +## Discovering Parameters + +Parameter schemas are visible in the tool definition at call time. + +## Trace and Session URLs + +`fs_session_url`, `trace_id`, and `trace_url` are returned by {{tool "live-connect"}}, {{tool "live-disconnect"}}, {{tool "live-view-navigate"}}, {{tool "live-view-new"}}, and {{tool "live-view-snapshot"}}. + +- **fs_session_url** — the raw Fullstory session URL. +- **trace_id** — the 12-char base62 id for this connection's trace. **Capture and reuse this** as the parent identifier for `comment-*` tools and as input to {{tool "review-open"}} later. The trailing path segment of `trace_url` is the same id. +- **trace_url** — a shareable link that opens the live viewer in a browser. **Always print this to the user** so they can watch the agent's browser in real time. + +After every connection is established — via {{tool "live-connect"}} or {{tool "live-view-new"}} (tunnel-first flow) — output the URL on its own line: + +``` +Viewer: {trace_url} +``` + +## Capture Status + +Every live tool response that touches a view includes a `capture_status` field — +this includes {{tool "live-connect"}}, {{tool "live-view-new"}}, {{tool "live-view-navigate"}}, +{{tool "live-view-snapshot"}}, and {{tool "live-view-screenshot"}}. Check it after **every** such +call (not just {{tool "live-connect"}}) and respond as follows: + +- `active`: proceed normally. +- `blocked`: tell the user to check capture quota and verify the target domain is allow listed in Subtext data capture settings. +- `snippet_not_found` or `api_unavailable`: tell the user something went wrong during setup and they should run onboarding again. +- any other status: something went wrong, try again + +This matters especially in the **tunnel-first flow**, where {{tool "live-connect"}} is +never called — it's easy to miss the status if you assume the check only applies +to that one tool. + +## Operator and Signals + +{{tool "live-signal"}} is the trace's read channel for human-side activity. Call it +between action loops to learn about new comments and the operator state. + +**Response shape:** + +```json +{ + "operator": "agent" | "human", + "operator_email": "...", // present when operator=human + "signals": [ + { + "type": "comment", "id": "...", "ts": "...", + "text": "...", "author_type": "user|agent", + "intent": "...", "resolved": false, "parent_id": "..." + } + ], + "cursor": "...", // round-trip back as `since` next call + "server_time": "..." +} +``` + +**Polling pattern.** + +1. First call after {{tool "live-connect"}}: omit `since` to baseline the cursor. +2. Subsequent calls: pass the previous response's `cursor` as `since`. Only signals newer than the cursor come back. +3. Comment signals carry full text and metadata inline — no follow-up {{tool "comment-list"}} is needed for the new ones. +4. Save the new `cursor` after every call. + +**Operator gate.** When `operator=human`, the user has taken browser control. The `live-act-*` input tools (click, fill, hover, keypress, drag, dialog, upload) return a structured error and **must not be retried**. Read-only tools ({{tool "live-view-snapshot"}}, {{tool "live-view-screenshot"}}, `live-log-*`, `live-net-*`, {{tool "live-signal"}}) keep working. Stay read-only and keep polling — when `operator` flips back to `agent`, resume normal work. + +{{tool "live-act-wait-for"}} is excluded from the gate (observation-only). + +## Tips + +- **Default to {{tool "live-view-snapshot"}} for all page observation.** It carries sightmap context (component names, view names, `[src: ...]` annotations) and provides the component UIDs needed by `act-*` tools. Always call it before any interaction sequence. +- **Use {{tool "live-view-screenshot"}} only for visual evidence** — before/after comparisons, layout debugging, or screenshots to embed in PRs. The tool returns the image inline by default so you can verify framing. Pass `upload:true` only when you need a hosted URL for a PR or comment attachment. +- When the screenshot is evidence about a specific element, clip to it with `component_id` (and small `expand_pct` for context). `expand_pct` caps at 100, so very short elements (a label, a textbox) still produce thin slices — clip to a wider parent in that case. +- {{tool "live-view-inspect"}} is for **sightmap authoring only** — it returns verbose CSS selectors on every node. Do not use it as a general snapshot replacement. Use it once to discover selectors, write your `.sightmap/` YAML, then use {{tool "live-view-snapshot"}} for everything else. +- Component names from sightmap appear in snapshots — use `[src: ...]` annotations to find source files. +- Close connections when done to free server resources. + +## Tunnel Setup + +When the hosted browser needs to reach `localhost` or local dev URLs, use the tunnel-first flow: + +1. Call {{tool "live-tunnel"}} — allocates a browser connection and returns `relayUrl` + `connectionId` +2. Call {{tool "tunnel-connect"}} on the **subtext-tunnel** MCP server with `relayUrl` and `allowedOrigins` (one or more local origins the tunnel may serve) +3. Call {{tool "live-view-new"}} with the `connection_id` and localhost URL + +Do **not** use {{tool "live-connect"}} for localhost URLs — it mints its own connection ID and can't bind to the tunnel. See `subtext:tunnel` for full details, including the trunk pattern (`host:port` covers all subdomains on the same port) needed when local apps redirect across subdomains (e.g. OAuth flows). + +## See Also + +- `subtext:shared` — MCP conventions and sightmap upload +- `subtext:tunnel` — Reverse tunnel setup for localhost access diff --git a/templates/skills/onboard/SKILL.md b/templates/skills/onboard/SKILL.md new file mode 100644 index 00000000..837b442e --- /dev/null +++ b/templates/skills/onboard/SKILL.md @@ -0,0 +1,154 @@ +--- +name: onboard +description: Interactive first-run onboarding for new Subtext users. Connects to the user's local dev server, proves a small change with before/after evidence in a watchable trace, then bootstraps a starter sightmap from what was learned. +metadata: + targets: [mcp] + platform: claude-code + requires: + skills: ["subtext:shared", "subtext:proof", "subtext:sightmap", "subtext:live", "subtext:tunnel"] +--- + +# Onboarding + +> **PREREQUISITE — Read inline before any other action:** Read skills `subtext:proof`, `subtext:sightmap`, `subtext:live`, `subtext:tunnel`, `subtext:shared`. + +**Type:** User-facing workflow. Conversational. Three visible steps. + +The goal: walk a new user through one real, useful Subtext run end-to-end. They watch live in the trace as the agent makes a small visible change to their running app, see before/after screenshots they can point at, and end up with a starter `.sightmap/` file as a natural byproduct of the work. The trace itself stays valid after the run as a replayable recording. + +## Implicit health check + +Do **not** announce a "plugin setup" step. Trust that the plugin is installed — the user just ran a slash command from it. If the first MCP call below fails (server unreachable, auth missing), invoke `subtext:setup-plugin`, then retry the call. Otherwise stay silent about plumbing. + +## Greeting + +Open with two short paragraphs — no banner, no checklist: + +> "Welcome to Subtext. +> +> Subtext helps me learn your product, validate changes against the actual running app, and leave proof of work — recorded sessions, before/after screenshots, comment markers, and a sightmap of your components — that you and downstream reviewers can replay. +> +> We're going to make one small visible change to your running app together. You'll watch it happen live in a trace you can replay later, see before/after screenshots, and end up with a starter sightmap your future agents can read. Should take about five minutes." + +## Step 1 — Connect to your local dev server + +Print: + +``` +--- + +## Step 1 of 3: Connect + +--- +``` + +Ask the user for the URL of their local dev server. Be explicit about **local**: + +> "What URL is your local dev server running at? Something like `http://localhost:3000`, `http://localhost:5173`, or whatever port you've got it on. +> +> It needs to be local — we'll be making a real code change in this session, and a hosted staging or production URL won't reflect a change you haven't shipped yet. If your server isn't running, fire it up now and come back." + +Wait for the user's answer. Do **not** probe ports yourself. Do **not** read `package.json` and guess. Starting the dev server is the user's responsibility; you just need the URL. + +If they paste a non-local URL, push back — explain the change won't be visible there — and ask again. + +Once you have a `http://localhost:…` (or `http://127.0.0.1:…`) URL, follow the **tunnel-first** flow from `subtext:tunnel`: + +1. `live-tunnel()` → `connectionId`, `relayUrl` +2. `tunnel-connect({ relayUrl, allowedOrigins: [] })` → confirm `state: "ready"` +3. `live-view-new({ connection_id, url: })` → returns `trace_url` (and the initial snapshot) + +For onboarding the user only navigates to the URL they gave us, so a single-entry allowlist is correct. If their app turns out to redirect across subdomains (OAuth, multi-host dev), `subtext:tunnel` covers the trunk pattern that implicitly matches every subdomain. + +If any of these calls fails because the MCP server is unreachable, invoke `subtext:setup-plugin`, then retry. + +**Print the `trace_url` immediately, on its own line, before saying anything else:** + +``` +Watch along here: +{trace_url} +``` + +Tell the user briefly: + +> "I'm connected. Open that link in another window — you'll watch live as I work. The same link stays valid as a replayable recording after we finish, so you can come back to it." + +## Step 2 — Make a small change + +Print: + +``` +--- + +## Step 2 of 3: Prove a Change + +--- +``` + +Ask for a small visible change. Give concrete examples so the user doesn't have to guess what counts: + +> "What's a small visible change you'd like to make? Things that work well: +> - **Text** — change a heading or label (e.g., 'Sign Up' → 'Get Started') +> - **Style** — color, spacing, sizing (e.g., make the primary button green; tighten padding around the hero) +> - **Visibility** — hide or remove an element (e.g., remove the footer copyright line) +> +> Keep it small — anything you can eyeball." + +Once the user describes the change, follow the **`subtext:proof`** workflow you already read inline. Important: + +- The connection from Step 1 is reusable. Skip proof's Step 1 (connect) and Step 2 (share trace URL — already done). Start at proof's Step 3 (BEFORE capture). +- Use the existing `connection_id` and `view_id` from Step 1 in {{tool "live-view-screenshot"}} and {{tool "comment-add"}} calls. + +When proof's loop completes, recap to the user in a single message: + +> "Done. +> +> - **Before:** {before_screenshot_url} +> - **After:** {after_screenshot_url} +> - **Trace:** {trace_url} +> +> Does the result look right?" + +If the user says no, ask whether to revert, retry, or stop. If yes, continue to Step 3. + +## Step 3 — Bootstrap a sightmap from what we just learned + +Print: + +``` +--- + +## Step 3 of 3: Sightmap + +--- +``` + +**Frame the why before writing anything.** Say something like: + +> "Now I'll capture what I just learned about your app in a small `.sightmap/` file. This is the artifact that makes the *next* run faster — the next coding agent (Claude, Cursor, Codex, anything that reads repo files) reads this YAML and already knows what these components are without exploring. They get committed to your repo so the head start travels with the code." + +Then create or extend `.sightmap/components.yaml` using the `subtext:sightmap` schema. Include: + +- **Component definitions** for elements you actually touched during the proof run. Use stable selectors — prefer `data-*` attributes when present. +- **Memory entries** about runtime behavior you observed — state changes, validation, gating, anything not obvious from the rendered DOM. Skip code-structure tips, file paths, JSX/CSS patterns, and external doc references; those don't belong in the sightmap. See `subtext:sightmap` for the full rule. + +Stay honest about scope: only describe what you actually touched. Don't pad the file with components you didn't interact with — those are best added when an agent works with them later, not speculatively now. + +After writing, show the user the file and a brief commit pointer: + +> "Wrote `.sightmap/components.yaml`. Commit it alongside your code change — it travels with the repo, and every future agent that reads it gets a head start." + +## Wrap-up + +Recap and point to next steps: + +> "You're set up. Recap: +> +> 1. **Trace** — the recorded session of the change you just made: {trace_url} +> 2. **Before / After** — {before_screenshot_url} → {after_screenshot_url} +> 3. **Sightmap** — `.sightmap/components.yaml` ready to commit +> +> From here: +> - **`/proof`** — use this any time you make a UI change. Same before/after evidence loop, no onboarding wrapper. +> - **`/review`** — paste any session URL to get a structured summary, with optional reproduction steps on request. +> - **Learn more about sightmap** — read `skills/sightmap/SKILL.md` to teach agents about more of your app's surface (views, requests, scoped components, memory entries)." diff --git a/templates/skills/proof/SKILL.md b/templates/skills/proof/SKILL.md new file mode 100644 index 00000000..72c73d50 --- /dev/null +++ b/templates/skills/proof/SKILL.md @@ -0,0 +1,325 @@ +--- +name: proof +description: You MUST use this skill when implementing, fixing, or refactoring code. Captures evidence artifacts (screenshots, network traces, code diffs, trace session links) into a proof document as you work. +metadata: + targets: [mcp, cli] + requires: + skills: ["subtext:shared", "subtext:live", "subtext:comments", "subtext:docs"] + mcp-server: subtext +--- + +# Proof + +> **PREREQUISITE — Read inline before any other action:** Read skills `subtext:shared`, `subtext:live`, `subtext:comments`, `subtext:docs`. + +**Type:** Rigid workflow — follow exactly. Skipping steps means unverified work ships. + +Every code change that affects what a user sees must be visually proven. This skill creates a before/after evidence trail that proves the change works. No exceptions, no "it should look fine." + +## Screenshot Capture + +**Always use {{tool "live-view-screenshot"}} with `upload: true`.** This uploads the screenshot to cloud storage and returns a signed URL you can attach to comments and PRs. + +{{if eq .Target "cli"}} +```bash +subtext live view-screenshot --upload +# → { "data": { "screenshot_url": "https://..." } } +``` +{{else}} +``` +live-view-screenshot({ connection_id, view_id, upload: true }) +→ { screenshot_url: "https://..." } +``` +{{end}} + +**Do NOT use {{tool "artifact-upload"}} for screenshots.** It requires base64-encoding the entire PNG and frequently fails on large images. The `upload: true` flag on {{tool "live-view-screenshot"}} handles the upload server-side — smaller payload, no encoding issues. + +**Pass `screenshot_url` through verbatim — query string included.** The full signed URL is the credential. Don't strip `?Expires=…&GoogleAccessId=…&Signature=…` when copying it into PR descriptions, comments, or summaries — without those params GCS returns 403 and the image won't render. + +To attach a screenshot to a comment: + +{{if eq .Target "cli"}} +```bash +URL=$(subtext live view-screenshot --upload | jq -r '.data.screenshot_url') +subtext comment add --screenshot_url "$URL" --intent looks-good --text "AFTER: ..." +``` +{{else}} +``` +live-view-screenshot({ ..., upload: true }) → screenshot_url +comment-add({ ..., screenshot_url, intent: "looks-good", text: "AFTER: ..." }) +``` +{{end}} + +## Proof Document + +Every proof run creates a permanent record alongside the live session. This lets you — and any future reviewer — reconstruct exactly what changed, what it looked like before and after, and what evidence backed the decision to ship. + +**Create once, attach continuously, close at the end.** Pass the `verification` seed template (see `subtext:docs`) unless you have a better fit: + +{{if eq .Target "cli"}} +```bash +subtext doc create --title "" --content "" +# → doc_id, doc_url ← save both +``` +{{else}} +``` +doc-create(title: , content: ) +→ doc_id, doc_url ← save both +``` +{{end}} + +The `doc_id` travels through every step below. The `doc_url` is the permanent link you hand to the user at the end. + +## The Loop + +### Step 1: Connect to the running app + +Open a browser connection per `subtext:live`'s connect flow — {{tool "live-connect"}} for remote URLs, tunnel-first ({{tool "live-tunnel"}} → {{tool "tunnel-connect"}} → {{tool "live-view-new"}}) for localhost. The hosted browser cannot reach localhost without the tunnel; see `subtext:live` for both flows in detail. + +If the app isn't running, **try to start the dev server yourself first.** Look for `package.json` scripts (`dev`, `start`, `serve`), a `Makefile`, or a `docker-compose.yml`. Run the appropriate command in the background. Only ask the user if you can't figure out how to start it. + +Read any existing comments with {{tool "comment-list"}} — prior feedback may inform your work. + +Call {{tool "doc-create"}} with the `verification` seed template (or `bug-fix` / `changeset` if the context fits better). Save `doc_id` and `doc_url`. + +### Step 2: Share the trace URL + +**Immediately** print the `trace_url` from the connect step on its own line. ({{tool "live-connect"}} returns it for remote; {{tool "live-view-new"}} returns it for tunnel-first.) This lets the user watch the agent's browser in real time and gives downstream reviewers{{if ne .Target "cli"}} (including `subtext:review` follow-ups){{end}} a stable entry point to the recorded session. + +``` +Trace: {trace_url} +I'm connected to the app. Starting verification. +``` + +Do NOT bury the link in a wall of text. It goes first, on its own line. + +### Polling discipline (Steps 3–6) + +While the trace is open, the human reviewer can leave comments or take browser control at any time. Between any two `live-*` calls in the loop below, call {{tool "live-signal"}} with the cursor saved from the previous call (omit `since` on the first call to baseline). New comments come back inline — read each, reply via {{tool "comment-reply"}} if it directs the work, and save the new `cursor`. The `operator` field on every response is the source of truth for control state. See `subtext:live` for the full response shape and operator-gate behavior. + +### Step 3: Navigate to the affected area and capture BEFORE + +Drive the browser to the exact page/component/state where the change will be visible. + +1. Navigate to the right URL, click through to the right state +2. Call {{tool "live-view-screenshot"}} with `upload: true` — this is the BEFORE evidence. Save the returned `screenshot_url`. +3. Call {{tool "comment-add"}} with intent `ask`, passing the `screenshot_url`: + - Text: "BEFORE: [describe current state]. About to make [describe planned change]." + - This is a chapter marker — it anchors the timeline for anyone reviewing the session later. +4. Attach to the proof document: +{{if eq .Target "cli"}} + ```bash + subtext doc attach --doc_id --section "Before" --render_as image --url --label "Before: " + ``` +{{else}} + ``` + doc-attach(doc_id, section: "Before", render_as: "image", url: {screenshot_url}, label: "Before: {description}") + ``` +{{end}} + +**Judgment call:** If the change affects multiple pages or states, capture BEFORE for each. + +### Step 4: Make the code change + +Edit files, update components, fix styles — whatever the task requires. + +This is the only step where you leave the browser and work in the codebase. + +After editing, attach the diff to the proof document: +{{if eq .Target "cli"}} +```bash +subtext doc attach --doc_id --section "Changes" --render_as link \ + --text "$(git diff)" --content_type text/plain \ + --label "" +``` +{{else}} +``` +doc-attach(doc_id, section: "Changes", render_as: "link", + text: {git diff output}, content_type: "text/plain", + label: {one-line description of what changed}) +``` +{{end}} + +If a `live-act-*` tool returns `Control transferred to human viewer`, the reviewer has taken control. Enter standby — do not retry. Continue polling {{tool "live-signal"}}; when `operator` flips back to `agent`, resume UI-facing work. Backend changes that don't need visual verification can continue while you wait. + +### Step 5: Verify the change with live tools + +Return to the browser. Refresh, hot reload, or reconnect if the dev server restarted. + +1. Navigate back to the same page/state from Step 3 +2. Call {{tool "live-view-screenshot"}} with `upload: true` — this is the AFTER evidence. Save the returned `screenshot_url`. +3. **Visually compare** BEFORE vs AFTER against the original prompt intent or acceptance criteria +4. Call {{tool "comment-add"}} with the AFTER `screenshot_url`: + - Intent: `looks-good` if it matches intent, `bug` if something is wrong + - Text: "AFTER: [describe what changed]. [Assessment against acceptance criteria]." +5. Attach to the proof document: +{{if eq .Target "cli"}} + ```bash + subtext doc attach --doc_id --section "After" --render_as image --url --label "After: " + ``` +{{else}} + ``` + doc-attach(doc_id, section: "After", render_as: "image", url: {screenshot_url}, label: "After: {description}") + ``` +{{end}} + +**Use live interaction tools to test the change:** + +- Click buttons, fill forms, hover elements — confirm the change works functionally, not just visually +- Check edge cases: empty states, long text, missing data +- If the change touches styles: check dark mode, mobile viewport (use {{tool "live-emulate"}}) + +### Step 6: Self-correct if needed + +If the AFTER doesn't match intent: + +1. Call {{tool "live-view-screenshot"}} with `upload: true`, then {{tool "comment-add"}} with intent `bug` and the `screenshot_url`: "ISSUE: [what's wrong]. Fixing now." +2. Attach the issue screenshot to the proof document: +{{if eq .Target "cli"}} + ```bash + subtext doc attach --doc_id --section "After" --render_as image --url --label "Issue: " + ``` +{{else}} + `doc-attach(doc_id, section: "After", render_as: "image", url: {screenshot_url}, label: "Issue: {what's wrong}")` +{{end}} +3. Go back to Step 4, make the fix +4. Return to Step 5, re-verify + +**Max 5 iterations.** If you can't get it right in 5 tries, stop and share what you have with the user. Don't spin. + +### Step 7: Package evidence and close the proof document + +Once the change is verified: + +1. Take a final {{tool "live-view-screenshot"}} with `upload: true` of the confirmed state +2. Call {{tool "comment-add"}} with intent `looks-good` and the `screenshot_url`: "VERIFIED: [summary of what was changed and confirmed]." +3. Attach the trace to the proof document: +{{if eq .Target "cli"}} + ```bash + subtext doc attach --doc_id --section "Evidence" --render_as link --url --label "Session trace" + ``` +{{else}} + `doc-attach(doc_id, section: "Evidence", render_as: "link", url: {trace_url}, label: "Session trace")` +{{end}} +4. Re-read the document ({{tool "doc-read"}}) and confirm a cold reviewer would understand what changed, what was tested, and why. Attach anything missing. +5. Close: +{{if eq .Target "cli"}} + ```bash + subtext doc close --doc_id --status complete --summary "" + ``` +{{else}} + `doc-close(doc_id, status: "complete", summary: {one sentence outcome})` +{{end}} +6. If a PR exists or will be created: + - Include before/after screenshot URLs (from Step 3 and Step 5) in the PR description + - Include the `trace_url` and `doc_url` so reviewers can watch the session and read the evidence record + +```markdown +## Visual Evidence + +**Before:** +![before]({before_screenshot_url}) + +**After:** +![after]({after_screenshot_url}) + +**Session replay:** [Review the full session]({trace_url}) +**Proof document:** {doc_url} +``` + +The `screenshot_url` values are signed URLs from {{tool "live-view-screenshot"}} with `upload: true`. They render directly in GitHub PR descriptions as inline images. + +## When to Use This Skill + +**Always use when you modify:** + +- Component files: `.tsx`, `.jsx`, `.vue`, `.svelte` +- Stylesheets: `.css`, `.scss`, `.less`, `.tailwind` +- Template/markup: `.html`, `.ejs` +- Any file that changes what renders on screen + +**Skip when:** + +- Change is purely backend (API handler, database query, utility function) +- Change is test-only (no production UI impact) +- Change is documentation-only + +**Not sure?** Use the skill. The cost of an unnecessary screenshot is near zero. The cost of shipping an unverified visual change is a bug report. + +## Comment Annotations as Chapter Markers + +Comments you leave during verification serve two purposes: + +1. **Live collaboration** — the user watching the trace URL sees your annotations in real-time in the sidebar +2. **Session replay chapters** — anyone reviewing the recorded session later can jump to your BEFORE/AFTER markers to understand what changed and why + +Leave comments at every significant moment: + +- BEFORE state captured +- Change made, verifying now +- ISSUE found (with screenshot) +- VERIFIED (with screenshot) + +Think of these as commit messages for visual state. + +## Decision Logic + +### Functional vs. aesthetic changes + +| Change type | Verification depth | Share with user? | +| ----------------------------------------------------------- | ------------------------------------------------------- | --------------------------------- | +| Functional fix (broken button, wrong text, missing handler) | Self-verify: BEFORE/AFTER screenshots + functional test | Only if it fails after 2 attempts | +| Aesthetic change (new component, colors, spacing, layout) | Full verify: BEFORE/AFTER + dark mode + mobile viewport | Always — aesthetic is subjective | +| Style-touching (CSS variables, theme classes, responsive) | Full verify + theme variants + viewport variants | Always — high regression risk | + +### Multiple affected areas + +If the change affects more than one page or state: + +1. List all affected areas +2. BEFORE screenshot each one +3. Make the change +4. AFTER screenshot each one +5. Any failure = back to Step 4 + +## Composition + +- **Requires:** `subtext:live` (browser tools, returns `trace_url`), `subtext:comments` (annotations), `subtext:docs` (proof document) +{{if ne .Target "cli"}}- **Hands off to:** `subtext:review` — when the session is complete, another agent (or the same agent later) can review the recorded session as a secondary verification pass +{{end}}- **Triggers from:** any file edit to UI code, or when the user asks for a visual change + +## Async heartbeat (Claude Code only, MANDATORY) + +The polling discipline above keeps you in sync as long as you're actively +calling `live-*` tools. Long idle gaps inside a proof run — multi-minute +compilations, deep code work without browser tool calls, waiting on a build — +can leave the agent unaware of comments or control changes that arrived during +the gap. + +On Claude Code, schedule an async heartbeat with `/loop` to cover that case: + +``` +/loop 60s call live-signal with trace_id= and the saved cursor; +if signals[] is non-empty, summarize and route any actionable comments; +if operator=human, note "user has control" and stop input actions; +if signals=null and operator=agent for 5 consecutive ticks, call +CronDelete and report "idle, loop stopped". +``` + +60s is the floor on Claude Code's scheduler. Stop the loop before Step 7 +(`/loop` stop, or whatever your harness exposes) so async ticks don't +interleave with the closing summary. + +**Idle-stop is the unhappy-path equivalent.** The trailing clause keeps a +forgotten loop from running all the way to its 7-day cron auto-expiry — if the +agent loses context, gets reassigned, or just forgets to stop the loop at Step +7, it self-terminates after ~5 quiet minutes (5 ticks × 60s) instead of polling +into the void. Pass the scheduled job id into `` at `/loop` time so the +prompt can call `CronDelete` against itself. Tune the threshold for the task: +5 is forgiving enough that brief AFK moments don't trigger it, short enough +that a forgotten loop doesn't burn tokens overnight. + +**Other harnesses.** Cursor, Codex, opencode, Gemini CLI, and Open SWE don't +expose an in-session scheduler equivalent to `/loop`. The in-context polling +discipline above is the supported path on those harnesses; the idle-gap case +isn't covered today and lands when the agent's next tool call fires. diff --git a/templates/skills/recipe-sightmap-setup/SKILL.cli.md b/templates/skills/recipe-sightmap-setup/SKILL.cli.md new file mode 100644 index 00000000..d70a6031 --- /dev/null +++ b/templates/skills/recipe-sightmap-setup/SKILL.cli.md @@ -0,0 +1,33 @@ +# Recipe: Sightmap Setup + +> **PREREQUISITE:** Read `subtext:sightmap` for the full schema reference. + +## Steps + +1. **Navigate to the page**: {{tool "live-view-navigate"}} or {{tool "live-view-new"}} +2. **Take a baseline snapshot**: {{tool "live-view-snapshot"}} to see the current a11y tree with generic roles +3. **Identify key UI components** in the snapshot (navigation, forms, cards, modals, etc.) +4. **Find good selectors** using {{tool "live-view-inspect"}} — this returns the full component tree with CSS selectors (tag, id, classes, `data-*` attributes, `aria-*`, `href`, etc.) on every node. Use it to identify stable targeting info, then switch back to {{tool "live-view-snapshot"}} for normal interaction. + Prefer `data-*` attributes when available — they're stable and semantically meaningful (e.g., `[data-component="ProductTile"]`, `[data-testid="checkout-button"]`). +5. **Create `.sightmap/components.yaml`** with component definitions (see `subtext:sightmap` skill for schema) +6. **Add memories** to key components — contextual notes that appear in a `[Guide]` section at the top of every snapshot. Focus on: + - **Auth/access**: passwords, test accounts, login flows (e.g., `"Password is 'argus'"`) + - **Stateful components**: how toggles, tabs, or modes change the UI (e.g., `"Audience toggle switches copy between builder/agent perspectives"`) + - **Forms**: required fields, validation rules, expected formats + - **Complex interactions**: multi-step flows, known quirks, non-obvious behavior + ```yaml + - name: LoginForm + selector: "[data-component='LoginForm']" + source: src/components/LoginForm.tsx + memory: + - "Test account: user@test.com / password123" + - "Shows CAPTCHA after 3 failed attempts" + ``` +7. **Upload the sightmap**: get the upload URL from `subtext live tunnel` or `subtext live connect`, then: + ```bash + subtext sightmap upload --url + ``` +8. **Take another snapshot** to verify component names appear (definitions are re-read on each snapshot) +9. **Add views** if the app has distinct routes — specify route patterns and view-scoped components +10. **Add requests** if key API endpoints should have semantic names — use {{tool "live-net-list"}} to identify them +11. **Verify enrichment**: take snapshots on different views, check `[View: ...]` headers, semantic names, and `[Guide]` section with memories all appear diff --git a/templates/skills/recipe-sightmap-setup/SKILL.md b/templates/skills/recipe-sightmap-setup/SKILL.md new file mode 100644 index 00000000..57493432 --- /dev/null +++ b/templates/skills/recipe-sightmap-setup/SKILL.md @@ -0,0 +1,38 @@ +--- +name: recipe-sightmap-setup +description: Short recipe to create sightmap definitions for a project from scratch. +metadata: + targets: [mcp, cli] + requires: + skills: ["subtext:sightmap"] +--- + +# Recipe: Sightmap Setup + +> **PREREQUISITE:** Read `subtext:sightmap` for the full schema reference. + +## Steps + +1. **Navigate to the page**: `live-view-navigate(url=...)` or `live-view-new(url=...)` +2. **Take a baseline snapshot**: `live-view-snapshot()` to see the current a11y tree with generic roles +3. **Identify key UI components** in the snapshot (navigation, forms, cards, modals, etc.) +4. **Find good selectors** using `live-view-inspect()` — this returns the full component tree with CSS selectors (tag, id, classes, `data-*` attributes, `aria-*`, `href`, etc.) on every node. Use it to identify stable targeting info, then switch back to `live-view-snapshot()` for normal interaction. + Prefer `data-*` attributes when available — they're stable and semantically meaningful (e.g., `[data-component="ProductTile"]`, `[data-testid="checkout-button"]`). +5. **Create `.sightmap/components.yaml`** with component definitions (see `subtext:sightmap` skill for schema) +6. **Add memories** to key components — contextual notes that appear in a `[Guide]` section at the top of every snapshot. Focus on: + - **Auth/access**: passwords, test accounts, login flows (e.g., `"Password is 'argus'"`) + - **Stateful components**: how toggles, tabs, or modes change the UI (e.g., `"Audience toggle switches copy between builder/agent perspectives"`) + - **Forms**: required fields, validation rules, expected formats + - **Complex interactions**: multi-step flows, known quirks, non-obvious behavior + ```yaml + - name: LoginForm + selector: "[data-component='LoginForm']" + source: src/components/LoginForm.tsx + memory: + - "Test account: user@test.com / password123" + - "Shows CAPTCHA after 3 failed attempts" + ``` +7. **Take another snapshot** to verify component names appear (definitions are re-read on each snapshot) +8. **Add views** if the app has distinct routes — specify route patterns and view-scoped components +9. **Add requests** if key API endpoints should have semantic names — use {{tool "live-net-list"}} to identify them +10. **Verify enrichment**: take snapshots on different views, check `[View: ...]` headers, semantic names, and `[Guide]` section with memories all appear diff --git a/templates/skills/review/SKILL.md b/templates/skills/review/SKILL.md new file mode 100644 index 00000000..4fce8e5e --- /dev/null +++ b/templates/skills/review/SKILL.md @@ -0,0 +1,139 @@ +--- +name: review +description: Review a completed Subtext session and produce a structured summary. Use when you have a session URL and want to understand what happened — whether to verify another agent's proof work, walk through a dev / staging / preview flow, or summarize a captured session. Optionally emits reproduction steps on request (execution lives in `subtext:live`). Skip for tasks that modify code (use `subtext:proof`) or drive a running app (use `subtext:live`). +metadata: + targets: [mcp] + requires: + skills: ["subtext:shared", "subtext:session", "subtext:comments"] + mcp-server: subtext +--- + +# Review + +> **PREREQUISITE:** Read `subtext:shared`, `subtext:session`, `subtext:comments` for tool conventions. + +**Type:** Workflow — goal-oriented with decision logic. + +Review a completed session. Produce a structured summary of what happened. Optionally emit reproduction steps when the user asks. Reproduction execution itself is `subtext:live`'s job — review is read-only analysis. + +## When to use + +**Use when:** +- User provides a session URL and wants to know what happened +- Another agent needs to verify work captured by `subtext:proof` +- An agent revisits its own session after completion to sanity-check the result +- The user asks for a walkthrough, summary, or diagnosis of a session +- Session source is any of: agent proof runs, local dev, staging, preview + +**Skip when:** +- The task modifies code — use `subtext:proof` instead +- The task drives a running app live — use `subtext:live` +- The user wants the repro *executed*, not just described — hand off to `subtext:live` + +## Relationship to `subtext:proof` + +`proof` is the inner loop: the working agent captures BEFORE/AFTER screenshots, leaves comment chapter markers, and checks its own work in real time as it edits code. + +`review` is the outer loop: after the session is complete, another agent (or the same agent later) opens the recorded session and produces an independent read. Complementary, not overlapping — proof proves, review verifies. + +## The loop + +### Step 1: Open the session + +Call {{tool "review-open"}} with whichever identifier you have — see `subtext:session` for the five accepted forms (`trace_id`, `session_url`, `device_id` + `session_id`, `email_address`, `user_uid`). When handed off from `subtext:proof`, prefer the `trace_id` from the proof run — no URL construction needed. + +### Step 2: Read the chapter markers + +Call {{tool "comment-list"}}. Existing comments serve as chapter markers. A session produced by `subtext:proof` will have a predictable spine: + +- `BEFORE:` — initial state +- `AFTER:` — post-change state +- `ISSUE:` — problems encountered mid-loop +- `VERIFIED:` — final confirmed state + +Let the markers drive your reading order. If they're absent (non-proof sessions), you'll need to read more broadly. + +### Step 3: Read the session content + +Use {{tool "review-view"}} at key timestamps. Prioritize in this order: + +1. **Chapter markers** — read the frames around each one +2. **Errors** — use {{tool "review-inspect"}} or console/network lookups for failure moments +3. **Navigation inflections** — page changes, route transitions, modals +4. **Before/after pairs** — {{tool "review-diff"}} between known anchor points is the most revealing read + +Don't sweep the entire session frame-by-frame. That's expensive and usually unnecessary. + +### Step 4: Assess + +Form a judgment on: + +- **What the session was trying to accomplish** — stated in chapter markers or inferable from behavior +- **Did it succeed** — any errors? Did the final state match the stated intent? +- **Notable moments** — anything surprising, confusing, or worth flagging to the user + +### Step 5: Produce the structured summary + +Output in this shape — the format matters because the primary downstream consumer is often another agent, and stable sections make hand-off predictable: + +```markdown +## Session Summary + +**Session:** +**Type:** +**Duration:** + +### What happened + + +### Errors + + +### Key moments +- `` — +- `` — <...> + +### Assessment + +``` + +### Step 6: Reproduction steps — only if asked + +If the user explicitly asks to reproduce, append a structured step list. **Do not execute.** Hand off to `subtext:live` with the steps as a prompt for the agent that will run them. + +```markdown +### Reproduction steps + +1. Navigate to +2. — e.g., "Click the 'Sign in' button" +3. — e.g., "Fill email field with " +4. — e.g., "Verify modal appears within 2s" +``` + +Write the steps as deterministic actions a `subtext:live`-driven agent can follow. Avoid subjective instructions ("look around"); use concrete selectors, URLs, and assertions. + +## Decision logic + +### Session with proof chapter markers vs. without + +| Signal | Reading strategy | +| ----------------------------------- | ------------------------------------------------- | +| BEFORE / AFTER / VERIFIED markers present | Use them as the spine. {{tool "review-diff"}} between pairs. | +| Only ISSUE markers | Agent was struggling — lead with what went wrong. | +| No markers (dev / staging / preview) | Read navigation and errors first, form the narrative yourself. | + +### Errors present + +Always lead with errors. An agent-consumer downstream will grep for this section first. + +### Repro steps requested + +If the user says "reproduce", "repro", "walk me through step by step", or "how do I hit this" → produce the step list in Step 6. + +If the user says "review", "summarize", "what happened" → stop at Step 5. Do not volunteer repro steps — they add length without being asked for. + +## Composition + +- **Invoked by:** user directly with a session URL, or as a downstream handoff from `subtext:proof` +- **Composes:** `subtext:session` (tool catalog for `review-*`), `subtext:comments` (chapter markers) +- **Hands off to:** `subtext:live` when the user wants steps executed, not just written diff --git a/templates/skills/session/SKILL.md b/templates/skills/session/SKILL.md new file mode 100644 index 00000000..6f3bf9ca --- /dev/null +++ b/templates/skills/session/SKILL.md @@ -0,0 +1,56 @@ +--- +name: session +description: Session replay tools for analyzing Fullstory session recordings. Sparse API catalog — tools are self-describing. +metadata: + targets: [mcp] + requires: + skills: ["subtext:shared"] +--- + +# Session Replay + +> **PREREQUISITE:** Read `subtext:shared` for MCP conventions and sightmap upload. + +API catalog for the session replay tools (all prefixed `review-`). These tools let you open sessions, inspect UI state at specific timestamps, and compare state across time. + +## MCP Tools + +| Tool | Description | +|------|-------------| +| {{tool "review-open"}} | Open a session for analysis. Returns event summaries, metadata, timestamps. | +| {{tool "review-view"}} | Capture UI state at a timestamp — component tree + screenshot. Pass `component_id` to clip to a specific element's bounding box; optional `expand_pct` (0–100) grows the clip rect outward for surrounding context, clamped to the viewport. | +| {{tool "review-inspect"}} | Component tree with full CSS selectors — for sightmap authoring only, not general use | +| {{tool "review-diff"}} | Compare UI state between two timestamps | +| {{tool "review-close"}} | Close the session and free resources | + +## Discovering Parameters + +Parameter schemas are visible in the tool definition at call time. + +## Session Input + +{{tool "review-open"}} accepts five mutually-exclusive identifiers. Pick the one that matches what you actually have on hand — they're all first-class: + +- `trace_id` — the 12-char base62 id from a prior {{tool "live-connect"}} or {{tool "review-open"}} response. Resolves to the underlying device:session via the trace store. Use this when you're staying inside the trace flow (e.g., re-opening for a follow-on review, or pivoting between live and review surfaces). +- `session_url` — a full FullStory session URL. Use this when you have a URL from outside the trace flow — a customer-shared link, a Slack paste, or a session you found in the app UI. +- `device_id` + `session_id` — both required together. Use when you have the raw ids but no URL. +- `email_address` / `user_uid` — looks up the user's most recent session. Use when you don't have a specific session in mind. + +All five paths return the same trace_id-keyed handle; the entry point doesn't change what comes out. + +### Always capture `trace_id` from the response + +{{tool "review-open"}} emits `trace_id:` in its response regardless of which path you used (best-effort — omitted only if no trace exists for the resolved session, which is rare). **Capture it on entry** — comment tools ({{tool "comment-add"}}/`list`/`reply`) take a `trace_id`, not a session URL, so threading the trace_id forward saves you a round-trip later. + +## Tips + +- Event summaries from {{tool "review-open"}} are cheap. {{tool "review-view"}} is expensive. Start with summaries. +- When investigating a single suspect element, clip with `component_id` (and small `expand_pct` for context). Smaller payload, sharper evidence. `expand_pct` caps at 100, so very short elements still produce thin slices — clip to a wider parent in that case. +- {{tool "review-diff"}} between before/after is the most revealing tool — it shows exactly what changed. +- Console errors and network failures in event summaries are highest-signal starting points. +- Component names in {{tool "review-view"}}/{{tool "review-diff"}} output include `[src: ...]` annotations — use these to find source files directly. +- Always close sessions when done to free server resources. + +## See Also + +- `subtext:shared` — MCP conventions and sightmap upload diff --git a/templates/skills/setup-plugin/SKILL.md b/templates/skills/setup-plugin/SKILL.md new file mode 100644 index 00000000..2c0a3279 --- /dev/null +++ b/templates/skills/setup-plugin/SKILL.md @@ -0,0 +1,79 @@ +--- +name: setup-plugin +description: Install the Subtext plugin and configure MCP servers. Authenticates via OAuth or API Key. +metadata: + targets: [mcp] + requires: + skills: ["subtext:shared"] +--- + +# Setup Plugin + +Install and verify the Subtext plugin/extension. Works for Claude Code, Cursor, Codex, and Gemini CLI. + +## Pre-check + +Verify the plugin is working by testing actual connectivity — do NOT read config files or plugin cache directories. + +**Step 1: Test MCP connectivity** + +Try calling a lightweight MCP tool to verify the server is reachable. For example, list the available tools on the `subtext` MCP server. If the call succeeds, the plugin is installed and connected. + +Check these servers: +- `subtext` — required (for review, live, comments) +- `subtext-tunnel` — optional (local tunnel client) + +If MCP tools are available, the plugin is working. Report which servers connected and move on. + +**Step 2: Verify local dependencies** + +Run in a single command: +```bash +python3 --version 2>&1 && python3 -c "import yaml; print('PyYAML OK')" 2>&1 +``` + +These are required by the sightmap upload script. If missing: +- No Python 3: suggest `brew install python@3.12` (macOS) +- No PyYAML: suggest `python3 -m pip install pyyaml` + +**If all pass:** report "Plugin is set up — MCP servers connected, dependencies OK." and exit. + +## Install Steps + +### Plugin not installed + +If MCP tools are not available, the plugin needs to be installed. The command depends on the platform. + + + +**Claude Code:** + +``` +/plugin marketplace add https://github.com/fullstorydev/subtext +/plugin install subtext@subtext-marketplace +``` + +**Gemini CLI:** + +``` +gemini extensions install https://github.com/fullstorydev/subtext +``` + +Note: Slash commands can't be executed by the agent — the user must run them directly. + +### MCP connectivity failed + +If the MCP connectivity test fails, tell the user: + +1. **MCP connectivity test failed** — the subtext MCP server did not respond. +2. **Double-check authentication settings** for the MCP servers in the tool configuration. +3. **To authenticate**, follow the OAuth flow provided by the tool being used (e.g. Claude Code, Cursor) to connect the MCP servers. Alternatively, manually configure an API key header value for the MCP server as described in the installation instructions. + +After the user has addressed authentication, re-run the MCP connectivity check to confirm everything works. + +## Explain + +After setup, explain what was installed: +- **Skills** — `proof` (before/after evidence for UI changes), `review` (structured summary of any session), `onboard` (first-run walkthrough), plus the underlying tool catalogs (`live`, `session`, `comments`, `tunnel`, `sightmap`) +- **MCP servers** — `subtext` (review, live, comments) and `subtext-tunnel` (local tunnel client) +- **Sightmap** — semantic component mapping, uploaded automatically after each connection. Created naturally as agents work; see `subtext:sightmap`. diff --git a/templates/skills/shared/SKILL.cli.md b/templates/skills/shared/SKILL.cli.md new file mode 100644 index 00000000..63d32cb3 --- /dev/null +++ b/templates/skills/shared/SKILL.cli.md @@ -0,0 +1,48 @@ +# Shared + +Foundation for all subtext skills. Read this when any workflow or recipe lists it in PREREQUISITE. + +## Command Groups + +All commands ship in the `subtext` binary. Groups by subcommand: + +| Namespace | Commands | +|-----------|---------| +| `subtext live` | Browser automation: {{tool "live-connect"}}, {{tool "live-disconnect"}}, `subtext live view-*`, `subtext live act-*`, `subtext live log-*`, `subtext live net-*`, {{tool "live-tunnel"}}, {{tool "live-emulate"}}, {{tool "live-eval-script"}} | +| `subtext comment` | Comments: {{tool "comment-add"}}, {{tool "comment-list"}}, {{tool "comment-reply"}}, {{tool "comment-resolve"}} | +| `subtext doc` | Proof documents: {{tool "doc-create"}}, {{tool "doc-update"}}, {{tool "doc-attach"}}, {{tool "doc-close"}}, {{tool "doc-read"}}, {{tool "doc-diff"}}, {{tool "doc-list"}} | +| `subtext tunnel` | Reverse tunnel (built-in): {{tool "tunnel-connect"}}, {{tool "tunnel-disconnect"}}, {{tool "tunnel-status"}} | +| `subtext sightmap` | Sightmap: `subtext sightmap upload` | + +## Sightmap Upload + +Two commands return a sightmap upload URL: + +| Command | Field | Format | +|---------|-------|--------| +| {{tool "live-connect"}} | `sightmap_upload_url:` | text line in response | +| {{tool "live-tunnel"}} | `sightmapUploadUrl` | JSON field in response | + +If the project has `.sightmap/` definitions, upload them after getting the URL and **before** proceeding (before {{tool "live-view-new"}} for the tunnel-first flow): + +```bash +URL=$(subtext live tunnel --format json | jq -r .data.sightmapUploadUrl) +subtext sightmap upload --url "$URL" +``` + +The upload uses a single-use token embedded in the URL — no additional auth is needed. Do NOT pass the `sightmap` parameter directly to {{tool "live-connect"}}. + +## Discovering Parameters + +Run `subtext --help` to see parameters for any command. For example: + +```bash +subtext live connect --help +subtext comment add --help +``` + +## Security Rules + +- Never expose API tokens, session tokens, or credentials in output +- Confirm with the user before any write operation that modifies production data +- Session URLs may contain sensitive user data — don't log or repeat them unnecessarily diff --git a/templates/skills/shared/SKILL.md b/templates/skills/shared/SKILL.md new file mode 100644 index 00000000..d9621e0f --- /dev/null +++ b/templates/skills/shared/SKILL.md @@ -0,0 +1,56 @@ +--- +name: shared +description: Foundation skill for the subtext plugin. MCP tool conventions, environment detection, security rules, and sightmap upload. +metadata: + targets: [mcp, cli] + +--- + +# Shared + +Foundation for all subtext skills. Read this when any workflow or recipe lists it in PREREQUISITE. + +## MCP Servers + +All tools are served from the **subtext** MCP server. A **subtext-eu1** variant exists for EU1 data center sessions (`app.eu1.fullstory.com`). The agent framework resolves tool prefixes automatically based on the configured MCP servers — you do not need to hardcode prefixes. + +## Sightmap Upload + +Three tools return a sightmap upload URL: + +| Tool | Field | Format | +|------|-------|--------| +| {{tool "review-open"}} | `sightmap_upload_url:` | text line in response | +| {{tool "live-connect"}} | `sightmap_upload_url:` | text line in response | +| {{tool "live-tunnel"}} | `sightmapUploadUrl` | JSON field in response | + +If the project has `.sightmap/` definitions, upload them via the side-band script after getting the URL and **before** proceeding (before {{tool "review-view"}}/{{tool "review-diff"}} for review flows; before {{tool "live-view-new"}} for the tunnel-first flow): + +```bash +python3 ${CLAUDE_PLUGIN_ROOT}/skills/shared/collect_and_upload_sightmap.py --url +``` + +The upload uses a single-use token embedded in the URL — no additional auth is needed. Do NOT pass the `sightmap` parameter directly to {{tool "review-open"}}/{{tool "live-connect"}}. + +## Tool Name Prefixes + +Tools within the subtext server are grouped by prefix: + +| Prefix | Tools | +|--------|-------| +| `review-` | Session replay: {{tool "review-open"}}, {{tool "review-view"}}, {{tool "review-diff"}}, {{tool "review-close"}} | +| `live-` | Browser automation: {{tool "live-connect"}}, {{tool "live-disconnect"}}, `live-view-*`, `live-act-*`, `live-log-*`, `live-net-*`, {{tool "live-tunnel"}}, {{tool "live-emulate"}}, {{tool "live-eval-script"}} | +| `comment-` | Comments: {{tool "comment-add"}}, {{tool "comment-list"}}, {{tool "comment-reply"}}, {{tool "comment-resolve"}} | +| `doc-` | Proof documents: {{tool "doc-create"}}, {{tool "doc-update"}}, {{tool "doc-attach"}}, {{tool "doc-close"}}, {{tool "doc-read"}}, {{tool "doc-diff"}}, {{tool "doc-list"}} | + +The **subtext-tunnel** MCP server (for the reverse tunnel client) is a separate stdio server with its own tool namespace. + +## Discovering MCP Tool Parameters + +Each MCP tool is self-describing. If you're unsure about parameters, the tool's schema is available at call time. Don't memorize parameter lists — consult the atomic skill (`subtext:session`, `subtext:live`, or `subtext:comments`) for which tools exist, then let the schema guide parameter usage. + +## Security Rules + +- Never expose API tokens, session tokens, or credentials in output +- Confirm with the user before any write operation that modifies production data +- Session URLs may contain sensitive user data — don't log or repeat them unnecessarily diff --git a/templates/skills/shared/collect_and_upload_sightmap.py b/templates/skills/shared/collect_and_upload_sightmap.py new file mode 100644 index 00000000..caf21783 --- /dev/null +++ b/templates/skills/shared/collect_and_upload_sightmap.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Collect .sightmap/ definitions and upload them to a Lidar MCP session. + +Walks all .sightmap/**/*.yaml files under a given root, parses component definitions, flattens +hierarchical children into compound CSS selectors suitable for the subtext MCP's NFA matcher, and +uploads the result to the sightmap upload endpoint. + +Usage: + python3 collect_and_upload_sightmap.py --url [--root DIR] + +The upload URL is returned by open_session / open_connection and includes a +single-use authentication token. +""" + +# Python 3.9 compat +from __future__ import annotations + +import argparse +import json +import os +import ssl +import sys +import urllib.error +import urllib.parse +import urllib.request +from typing import Optional + +try: + import yaml # type: ignore[import-not-found] +except ImportError: + sys.exit("PyYAML is required: pip install pyyaml") + + +# --------------------------------------------------------------------------- +# Sightmap collection +# --------------------------------------------------------------------------- + + +def find_sightmap_files(root: str) -> list[str]: + """Find all .yaml/.yml files under root/.sightmap/. + + Checks only the direct .sightmap/ child of root to avoid walking + potentially massive directory trees (node_modules, go, etc.). + """ + sdir = os.path.join(root, ".sightmap") + if not os.path.isdir(sdir): + return [] + + files = [] + for dirpath, _, filenames in os.walk(sdir): + for name in sorted(filenames): + if name.endswith((".yaml", ".yml")): + files.append(os.path.join(dirpath, name)) + return files + + +def flatten_components( + components: list[dict], + parent_selectors: Optional[list[str]] = None, + parent_source: str = "", +) -> list[dict]: + """Flatten hierarchical component definitions into a flat list. + + Children inherit the parent's selectors as prefixes (descendant combinator) + and the parent's source if they don't specify their own. + + The YAML ``selector`` field may be a string or a list of strings. The output + always uses ``selectors`` (a JSON array) so the Go side never needs to split + comma-separated values. + """ + if parent_selectors is None: + parent_selectors = [] + result = [] + for comp in components: + name = comp.get("name", "") + raw = comp.get("selector", "") + source = comp.get("source", "") or parent_source + + # Normalise to a list — YAML authors may write a string or a list. + if isinstance(raw, list): + selectors = [s for s in raw if s] + elif raw: + selectors = [raw] + else: + selectors = [] + + # Build full selector chains by combining with parent selectors. + if parent_selectors and selectors: + full_selectors = [f"{p} {s}" for p in parent_selectors for s in selectors] + elif parent_selectors: + full_selectors = list(parent_selectors) + else: + full_selectors = selectors + + if name and full_selectors: + memory = comp.get("memory", []) + if not isinstance(memory, list): + memory = [memory] if memory else [] + entry = { + "name": name, + "selectors": full_selectors, + "source": source or "", + "memory": memory, + } + result.append(entry) + + # Recurse into children + children = comp.get("children", []) + if children: + result.extend(flatten_components(children, full_selectors, source)) + + return result + + +def parse_file(path: str) -> list[dict]: + """Parse a single sightmap YAML file and return flattened components.""" + with open(path) as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + return [] + + components = data.get("components", []) + if not isinstance(components, list): + components = [] + + result = flatten_components(components) + + # Also flatten view-scoped components + views = data.get("views", []) + if isinstance(views, list): + for view in views: + view_components = view.get("components", []) + if isinstance(view_components, list): + result.extend(flatten_components(view_components)) + + return result + + +def collect(root: str) -> list[dict]: + """Collect all sightmap definitions from a root directory.""" + files = find_sightmap_files(root) + result = [] + for path in files: + result.extend(parse_file(path)) + return result + + +def collect_memory(root: str) -> list[str]: + """Collect top-level memory entries from .sightmap/ YAML files.""" + files = find_sightmap_files(root) + result: list[str] = [] + for path in files: + with open(path) as f: + data = yaml.safe_load(f) + if not isinstance(data, dict): + continue + memory = data.get("memory", []) + if isinstance(memory, str): + memory = [memory] + if isinstance(memory, list): + result.extend(str(m) for m in memory if m) + return result + + +# --------------------------------------------------------------------------- +# Sightmap root discovery +# --------------------------------------------------------------------------- + + +def find_sightmap_root(cwd: str) -> Optional[str]: + """Find a directory containing .sightmap/, checking cwd and ancestors.""" + d = cwd + while d != os.path.dirname(d): + if os.path.isdir(os.path.join(d, ".sightmap")): + return d + d = os.path.dirname(d) + return None + + +# --------------------------------------------------------------------------- +# Upload +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Collect and upload .sightmap/ definitions" + ) + parser.add_argument( + "--url", + required=True, + help="Sightmap upload URL (from open_session/open_connection response)", + ) + parser.add_argument( + "--root", + default=None, + help="Root directory containing .sightmap/ (auto-detected if omitted)", + ) + args = parser.parse_args() + + root = ( + args.root or os.environ.get("SIGHTMAP_ROOT") or find_sightmap_root(os.getcwd()) + ) + if not root: + print("No .sightmap/ directory found", file=sys.stderr) + sys.exit(1) + + components = collect(root) + memory = collect_memory(root) + + if not components and not memory: + print("No sightmap definitions found") + sys.exit(0) + + body = json.dumps( + { + "sightmap": components, + "memory": memory, + } + ).encode("utf-8") + + req = urllib.request.Request( + args.url, + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + + # Allow self-signed certs for local dev servers (.test, localhost). + ssl_ctx = None + parsed_url = urllib.parse.urlparse(args.url) + if parsed_url.hostname and ( + parsed_url.hostname.endswith(".test") + or parsed_url.hostname in ("localhost", "127.0.0.1") + ): + ssl_ctx = ssl.create_default_context() + ssl_ctx.check_hostname = False + ssl_ctx.verify_mode = ssl.CERT_NONE + + try: + with urllib.request.urlopen(req, timeout=30, context=ssl_ctx) as resp: + result = json.loads(resp.read()) + count = result.get("components", 0) + print(f"Uploaded {count} sightmap component(s)") + except urllib.error.HTTPError as e: + body_text = e.read().decode("utf-8", errors="replace") + print(f"Upload failed ({e.code}): {body_text}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"Upload failed: {e.reason}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/templates/skills/shared/test_collect_sightmap.py b/templates/skills/shared/test_collect_sightmap.py new file mode 100644 index 00000000..88a0e690 --- /dev/null +++ b/templates/skills/shared/test_collect_sightmap.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Tests for sightmap collection in collect_and_upload_sightmap.py.""" + +import os +import sys +import tempfile + +# Import from the merged script in the same directory +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from collect_and_upload_sightmap import ( + collect, + find_sightmap_files, + flatten_components, + parse_file, +) + +TESTDATA = os.path.join(os.path.dirname(__file__), "testdata") + + +# --- flatten_components --- + + +class TestFlattenComponents: + def test_single_component(self): + result = flatten_components([ + {"name": "NavBar", "selector": "nav.main-nav", "source": "a.tsx"}, + ]) + assert result == [ + {"name": "NavBar", "selectors": ["nav.main-nav"], "source": "a.tsx", "memory": []}, + ] + + def test_children_inherit_parent_selector(self): + result = flatten_components([ + { + "name": "NavBar", + "selector": "nav.main-nav", + "source": "a.tsx", + "children": [ + {"name": "NavLink", "selector": "a.nav-link"}, + ], + }, + ]) + assert len(result) == 2 + assert result[0] == {"name": "NavBar", "selectors": ["nav.main-nav"], "source": "a.tsx", "memory": []} + assert result[1] == {"name": "NavLink", "selectors": ["nav.main-nav a.nav-link"], "source": "a.tsx", "memory": []} + + def test_children_inherit_parent_source(self): + result = flatten_components([ + { + "name": "Parent", + "selector": "div.parent", + "source": "parent.tsx", + "children": [ + {"name": "Child", "selector": "span.child"}, + ], + }, + ]) + assert result[1]["source"] == "parent.tsx" + + def test_child_overrides_source(self): + result = flatten_components([ + { + "name": "Parent", + "selector": "div.parent", + "source": "parent.tsx", + "children": [ + {"name": "Child", "selector": "span.child", "source": "child.tsx"}, + ], + }, + ]) + assert result[1]["source"] == "child.tsx" + + def test_deeply_nested_children(self): + result = flatten_components([ + { + "name": "A", + "selector": "div.a", + "source": "a.tsx", + "children": [ + { + "name": "B", + "selector": "div.b", + "children": [ + {"name": "C", "selector": "div.c"}, + ], + }, + ], + }, + ]) + assert len(result) == 3 + assert result[0]["selectors"] == ["div.a"] + assert result[1]["selectors"] == ["div.a div.b"] + assert result[2]["selectors"] == ["div.a div.b div.c"] + assert result[2]["source"] == "a.tsx" + + def test_no_source_produces_empty_string(self): + result = flatten_components([ + {"name": "X", "selector": "div.x"}, + ]) + assert result == [{"name": "X", "selectors": ["div.x"], "source": "", "memory": []}] + + def test_missing_name_skipped(self): + assert flatten_components([{"selector": "div.x"}]) == [] + + def test_missing_selector_skipped(self): + assert flatten_components([{"name": "X"}]) == [] + + def test_empty_list(self): + assert flatten_components([]) == [] + + def test_parent_selectors_passthrough(self): + result = flatten_components( + [{"name": "Child", "selector": "span.child"}], + parent_selectors=["div.parent"], + ) + assert result[0]["selectors"] == ["div.parent span.child"] + + def test_multiple_siblings(self): + result = flatten_components([ + {"name": "A", "selector": "div.a"}, + {"name": "B", "selector": "div.b"}, + {"name": "C", "selector": "div.c"}, + ]) + assert len(result) == 3 + assert [r["name"] for r in result] == ["A", "B", "C"] + + def test_yaml_list_selector(self): + result = flatten_components([ + {"name": "Sidebar", "selector": [".sidebar-a", ".sidebar-b"], "source": "s.tsx"}, + ]) + assert result == [ + {"name": "Sidebar", "selectors": [".sidebar-a", ".sidebar-b"], "source": "s.tsx", "memory": []}, + ] + + def test_yaml_list_selector_with_parent(self): + result = flatten_components( + [{"name": "Child", "selector": [".a", ".b"]}], + parent_selectors=[".p1", ".p2"], + ) + assert result[0]["selectors"] == [".p1 .a", ".p1 .b", ".p2 .a", ".p2 .b"] + + def test_list_parent_string_child(self): + result = flatten_components([ + { + "name": "Settings", + "selector": [".settings-a", ".settings-b"], + "source": "s.tsx", + "children": [ + {"name": "Item", "selector": ".item"}, + ], + }, + ]) + assert len(result) == 2 + assert result[0]["selectors"] == [".settings-a", ".settings-b"] + assert result[1]["selectors"] == [".settings-a .item", ".settings-b .item"] + + +# --- parse_file --- + + +class TestParseFile: + def test_navbar_yaml(self): + result = parse_file(os.path.join(TESTDATA, ".sightmap", "navbar.yaml")) + names = [r["name"] for r in result] + assert "NavBar" in names + assert "NavLink" in names + assert "NavLogo" in names + + nav_link = next(r for r in result if r["name"] == "NavLink") + assert nav_link["selectors"] == ["nav.main-nav a.nav-link"] + assert nav_link["source"] == "src/NavBar.tsx" + + nav_logo = next(r for r in result if r["name"] == "NavLogo") + assert nav_logo["source"] == "src/Logo.tsx" + + def test_views_yaml(self): + result = parse_file(os.path.join(TESTDATA, ".sightmap", "views.yaml")) + names = [r["name"] for r in result] + assert "Footer" in names + assert "CheckoutForm" in names + assert "SubmitButton" in names + + submit = next(r for r in result if r["name"] == "SubmitButton") + assert submit["selectors"] == ["form.checkout button.submit"] + assert submit["source"] == "src/Checkout.tsx" + + def test_empty_yaml(self): + assert parse_file(os.path.join(TESTDATA, ".sightmap", "empty.yaml")) == [] + + def test_nested_dashboard(self): + result = parse_file( + os.path.join(TESTDATA, "packages", "dashboard", ".sightmap", "dashboard.yaml") + ) + names = [r["name"] for r in result] + assert "Sidebar" in names + assert "SidebarMenu" in names + assert "MenuItem" in names + + menu_item = next(r for r in result if r["name"] == "MenuItem") + assert menu_item["selectors"] == ["aside.sidebar ul.menu li.menu-item"] + assert menu_item["source"] == "packages/dashboard/src/Sidebar.tsx" + + +# --- find_sightmap_files --- + + +class TestFindSightmapFiles: + def test_finds_root_sightmap(self): + files = find_sightmap_files(TESTDATA) + basenames = [os.path.basename(f) for f in files] + assert "navbar.yaml" in basenames + assert "views.yaml" in basenames + assert "empty.yaml" in basenames + + def test_finds_nested_sightmap(self): + basenames = [os.path.basename(f) for f in find_sightmap_files(TESTDATA)] + assert "dashboard.yaml" in basenames + + def test_all_yaml_extensions(self): + for f in find_sightmap_files(TESTDATA): + assert f.endswith((".yaml", ".yml")) + + def test_empty_dir(self): + with tempfile.TemporaryDirectory() as tmp: + assert find_sightmap_files(tmp) == [] + + def test_dir_without_sightmap(self): + with tempfile.TemporaryDirectory() as tmp: + os.makedirs(os.path.join(tmp, "src")) + assert find_sightmap_files(tmp) == [] + + +# --- collect (integration) --- + + +class TestCollect: + def test_collects_all_components(self): + result = collect(TESTDATA) + names = [r["name"] for r in result] + assert "NavBar" in names + assert "NavLink" in names + assert "Footer" in names + assert "CheckoutForm" in names + assert "Sidebar" in names + assert "MenuItem" in names + + def test_no_duplicates(self): + result = collect(TESTDATA) + pairs = [(r["name"], tuple(r["selectors"])) for r in result] + assert len(pairs) == len(set(pairs)) + + def test_empty_root(self): + with tempfile.TemporaryDirectory() as tmp: + assert collect(tmp) == [] diff --git a/templates/skills/shared/testdata/.sightmap/cards.yml b/templates/skills/shared/testdata/.sightmap/cards.yml new file mode 100644 index 00000000..2637911b --- /dev/null +++ b/templates/skills/shared/testdata/.sightmap/cards.yml @@ -0,0 +1,9 @@ +version: 1 + +components: + - name: ProductCard + selector: "div.card" + source: src/Card.tsx + - name: CardImage + selector: "div.card img.card-img" + source: src/Card.tsx diff --git a/templates/skills/shared/testdata/.sightmap/empty.yaml b/templates/skills/shared/testdata/.sightmap/empty.yaml new file mode 100644 index 00000000..b744e412 --- /dev/null +++ b/templates/skills/shared/testdata/.sightmap/empty.yaml @@ -0,0 +1 @@ +not_components: "this file has no components key" diff --git a/templates/skills/shared/testdata/.sightmap/navbar.yaml b/templates/skills/shared/testdata/.sightmap/navbar.yaml new file mode 100644 index 00000000..b283a0c2 --- /dev/null +++ b/templates/skills/shared/testdata/.sightmap/navbar.yaml @@ -0,0 +1,12 @@ +version: 1 + +components: + - name: NavBar + selector: "nav.main-nav" + source: src/NavBar.tsx + children: + - name: NavLink + selector: "a.nav-link" + - name: NavLogo + selector: "img.logo" + source: src/Logo.tsx diff --git a/templates/skills/shared/testdata/.sightmap/views.yaml b/templates/skills/shared/testdata/.sightmap/views.yaml new file mode 100644 index 00000000..c3afddf1 --- /dev/null +++ b/templates/skills/shared/testdata/.sightmap/views.yaml @@ -0,0 +1,17 @@ +version: 1 + +components: + - name: Footer + selector: "footer.site-footer" + source: src/Footer.tsx + +views: + - name: checkout + route: "/checkout" + components: + - name: CheckoutForm + selector: "form.checkout" + source: src/Checkout.tsx + children: + - name: SubmitButton + selector: "button.submit" diff --git a/templates/skills/shared/testdata/packages/dashboard/.sightmap/dashboard.yaml b/templates/skills/shared/testdata/packages/dashboard/.sightmap/dashboard.yaml new file mode 100644 index 00000000..5d545b95 --- /dev/null +++ b/templates/skills/shared/testdata/packages/dashboard/.sightmap/dashboard.yaml @@ -0,0 +1,12 @@ +version: 1 + +components: + - name: Sidebar + selector: "aside.sidebar" + source: packages/dashboard/src/Sidebar.tsx + children: + - name: SidebarMenu + selector: "ul.menu" + children: + - name: MenuItem + selector: "li.menu-item" diff --git a/templates/skills/sightmap/SKILL.cli.md b/templates/skills/sightmap/SKILL.cli.md new file mode 100644 index 00000000..e5af73cb --- /dev/null +++ b/templates/skills/sightmap/SKILL.cli.md @@ -0,0 +1,183 @@ +# Sightmap + +## Why this exists + +`sitemap.xml` tells search engines how to crawl your site. `.sightmap/` **teaches** agents how to use it. + +A `.sightmap/` directory at the project root is a small set of YAML files that name your app's views, components, and API routes — checked in alongside your code, learned from the running app, and read by every coding agent that touches the repo. Each definition can carry a `memory:` list: freeform notes about quirks, invariants, and shortcuts the source code doesn't record. + +What you get: + +- Snapshots and network traces show **semantic names** (`NavBar`, `CheckoutForm`, `FetchFlights`) instead of generic a11y roles (`navigation`, `region`, `generic`). +- A `[Guide]` section at the top of every enriched snapshot surfaces the `memory:` entries on whatever components are visible — so the next agent picks up where the last one left off (auth gates, state quirks, validation rules). +- The artifact is a few small YAML files in your repo. It travels with the code, works in any agent (Claude, Cursor, Codex, anything that reads files), and is curated incrementally as agents work — not authored up-front. + +## Uploading definitions + +After obtaining a sightmap upload URL from {{tool "live-tunnel"}} or {{tool "live-connect"}}, upload with: + +```bash +subtext sightmap upload --url +``` + +`subtext sightmap upload` auto-discovers `.sightmap/` from the current working directory. Pass `--root ` to point at a different directory. + +The upload uses a single-use token embedded in the URL — no additional auth is needed. + +## What you define + +The `.sightmap` maps selectors, URL patterns, and API routes to semantic names that agents and analytics tools share across sessions. Three definition types: + +- **Components** — map CSS selectors to semantic names (e.g., `NavBar`, `SearchBox`) +- **Views** — map URL route patterns to screen names (e.g., `ProductDetail`, `UserSettings`) +- **Requests** — map API endpoints to semantic names with payload schemas (e.g., `FetchFlights`, `CreateOrder`) + +## File Location + +Place definition files anywhere under `.sightmap/` in the project root. All `*.yaml` and `*.yml` files are discovered recursively and merged. Organize however makes sense for your project: + +``` +.sightmap/ + components.yaml # global components (NavBar, Footer) + views.yaml # view definitions with scoped components + pages/ + search.yaml # components specific to search page + cart.yaml # components specific to cart page +``` + +All files use the same schema and can contain `components`, `views`, `requests`, or any combination. The directory structure is for human organization — at load time everything merges. + +## Components + +Components map CSS selectors to semantic names. They can be **global** (top-level `components` array) or **view-scoped** (nested inside a view definition). + +### Schema + +```yaml +version: 1 +components: + - name: NavBar + selector: "nav.main-navigation" + source: src/components/NavBar.tsx + description: Main site navigation with links and action buttons + children: + - name: nav-link + selector: "a.nav-link" + - name: nav-button + selector: "button.nav-btn" + + - name: ProductCard + selector: ".product-card" + source: src/components/ProductCard.tsx + description: Reusable product display (search results, recommendations, home page) + + - name: PromotedProduct + selector: ".product-card.promo" + source: src/components/ProductCard.tsx + description: Promoted/featured variant of ProductCard +``` + +### Fields + +- **version** (required): Must be `1` +- **components** (optional): Array of component definitions + - **name** (required): Semantic name shown in snapshots (replaces a11y role) + - **selector** (required): CSS selector to match elements. May be a string or a YAML list of strings for multiple alternatives (avoids ambiguity with commas in selectors). + - **source** (optional): Relative path to the source file implementing this component. Not uploaded to the server, but useful for agents navigating source code locally. + - **description** (optional): Brief description of the component's purpose. Not uploaded, but useful for agents reading the sightmap directly. + - **memory** (optional): List of contextual notes about this component, uploaded and shown in a Component Guide section of snapshot output. + - **children** (optional): Child components. Their selectors are scoped to the parent's subtree. + +### Multiple matches + +When multiple definitions match the same element, all names are shown. For example, a `.product-card.promo` element matches both `ProductCard` and `PromotedProduct`: + +``` +uid=1_20 ProductCard, PromotedProduct "Cool Shoes" visible interactive +``` + +### Selector tips + +- Prefer stable selectors: `data-` attributes, semantic class names, element roles +- Avoid fragile selectors: deeply nested paths, nth-child, generated class names +- Use a YAML list for multiple matching patterns: + ```yaml + selector: + - ".search-bar" + - "[role='search']" + ``` +- Children selectors are automatically scoped to parent subtree + +### Writing memory entries + +A memory entry should help the **next agent driving or reviewing the running app** understand what's on screen — *runtime behavior*, not source structure. Useful gut check: would this note show up usefully in the `[Guide]` of a snapshot the agent's about to interact with? If the answer requires holding the codebase in hand too, it belongs in source comments or `CLAUDE.md`, not here. + +**Good memory candidates:** stateful behavior (how toggles change the rendered UI), auth gates and credentials, form rules, multi-step interactions, runtime quirks. + +**Stay out of memory:** file paths, JSX/CSS patterns, style conventions, external doc references — all discoverable elsewhere or owned by other artifacts. + +## Views + +A view represents a screen or route in the application. Views provide: + +1. **"You are here" context** — the snapshot header identifies the current view by name +2. **Scoped component definitions** — components that only exist on certain views +3. **Metadata** — description, source file reference + +### Schema + +```yaml +version: 1 + +# Global components — matched on all views +components: + - name: NavBar + selector: "nav.main-nav" + +views: + - name: HomePage + route: "/" + source: pages/Home.tsx + components: + - name: HeroSection + selector: ".hero-section" + + - name: ProductDetail + route: "/products/*" + source: pages/ProductDetail.tsx + components: + - name: AddToCartButton + selector: "button.add-to-cart" +``` + +### View Fields + +- **name** (required): Semantic name shown in snapshot header +- **route** (required): Glob pattern matched against URL pathname +- **description** (optional): Brief description of the view +- **source** (optional): Relative path to the source file +- **components** (optional): View-scoped component definitions (additive model) + +### Route matching + +- `*` matches a single path segment; `**` matches any depth +- First matching view wins (definition order = priority) + +## Requests + +Requests map API endpoints to semantic names. See the MCP version of this skill for the full schema. The upload format is identical — `subtext sightmap upload` reads the same `.sightmap/` YAML files. + +## Enriched Snapshot Output + +With a matched view: + +``` +[View: ProductDetail "https://mystore.com/products/123"] + +uid=1_0 RootWebArea "Blue Widget - MyStore" + uid=1_1 NavBar visible interactive + uid=1_10 main visible + uid=1_20 AddToCartButton "Add to Cart" visible interactive +``` + +Without definitions, elements still get `visible`/`interactive` annotations but use generic a11y roles. diff --git a/templates/skills/sightmap/SKILL.md b/templates/skills/sightmap/SKILL.md new file mode 100644 index 00000000..32f1600f --- /dev/null +++ b/templates/skills/sightmap/SKILL.md @@ -0,0 +1,347 @@ +--- +name: sightmap +description: Use when setting up the sight map (.sightmap/ YAML files) — defining components, views, requests, or other runtime semantics for the application. Also use when snapshot output shows generic a11y roles instead of meaningful names. +metadata: + targets: [mcp, cli] + +--- + +# Sightmap + +## Why this exists + +`sitemap.xml` tells search engines how to crawl your site. `.sightmap/` **teaches** agents how to use it. + +A `.sightmap/` directory at the project root is a small set of YAML files that name your app's views, components, and API routes — checked in alongside your code, learned from the running app, and read by every coding agent that touches the repo. Each definition can carry a `memory:` list: freeform notes about quirks, invariants, and shortcuts the source code doesn't record. + +What you get: + +- Snapshots and network traces show **semantic names** (`NavBar`, `CheckoutForm`, `FetchFlights`) instead of generic a11y roles (`navigation`, `region`, `generic`). +- A `[Guide]` section at the top of every enriched snapshot surfaces the `memory:` entries on whatever components are visible — so the next agent picks up where the last one left off (auth gates, state quirks, validation rules). +- The artifact is a few small YAML files in your repo. It travels with the code, works in any agent (Claude, Cursor, Codex, anything that reads files), and is curated incrementally as agents work — not authored up-front. + +## What you define + +The `.sightmap` maps selectors, URL patterns, and API routes to semantic names that agents and analytics tools share across sessions. Three definition types: + +- **Components** — map CSS selectors to semantic names (e.g., `NavBar`, `SearchBox`) +- **Views** — map URL route patterns to screen names (e.g., `ProductDetail`, `UserSettings`) +- **Requests** — map API endpoints to semantic names with payload schemas (e.g., `FetchFlights`, `CreateOrder`) + +## File Location + +Place definition files anywhere under `.sightmap/` in the project root. All `*.yaml` and `*.yml` files are discovered recursively and merged. Organize however makes sense for your project: + +``` +.sightmap/ + components.yaml # global components (NavBar, Footer) + views.yaml # view definitions with scoped components + pages/ + search.yaml # components specific to search page + cart.yaml # components specific to cart page +``` + +All files use the same schema and can contain `components`, `views`, `requests`, or any combination. The directory structure is for human organization — at load time everything merges. + +## Components + +Components map CSS selectors to semantic names. They can be **global** (top-level `components` array) or **view-scoped** (nested inside a view definition). + +### Schema + +```yaml +version: 1 +components: + - name: NavBar + selector: "nav.main-navigation" + source: src/components/NavBar.tsx + description: Main site navigation with links and action buttons + children: + - name: nav-link + selector: "a.nav-link" + - name: nav-button + selector: "button.nav-btn" + + - name: ProductCard + selector: ".product-card" + source: src/components/ProductCard.tsx + description: Reusable product display (search results, recommendations, home page) + + - name: PromotedProduct + selector: ".product-card.promo" + source: src/components/ProductCard.tsx + description: Promoted/featured variant of ProductCard +``` + +### Fields + +- **version** (required): Must be `1` +- **components** (optional): Array of component definitions + - **name** (required): Semantic name shown in snapshots (replaces a11y role) + - **selector** (required): CSS selector to match elements. May be a string or a YAML list of strings for multiple alternatives (avoids ambiguity with commas in selectors). + - **source** (optional): Relative path to the source file implementing this component. Not uploaded to the MCP server, but useful for agents navigating source code locally. + - **description** (optional): Brief description of the component's purpose. Not uploaded, but useful for agents reading the sightmap directly. + - **memory** (optional): List of contextual notes about this component, uploaded and shown in a Component Guide section of snapshot output. + - **children** (optional): Child components. Their selectors are scoped to the parent's subtree. + +### Multiple matches + +When multiple definitions match the same element, all names are shown. For example, a `.product-card.promo` element matches both `ProductCard` and `PromotedProduct`: + +``` +uid=1_20 ProductCard, PromotedProduct "Cool Shoes" visible interactive +``` + +### Selector tips + +- Prefer stable selectors: `data-` attributes, semantic class names, element roles +- Avoid fragile selectors: deeply nested paths, nth-child, generated class names +- Use a YAML list for multiple matching patterns: + ```yaml + selector: + - ".search-bar" + - "[role='search']" + ``` +- Children selectors are automatically scoped to parent subtree + +### Writing memory entries + +A memory entry should help the **next agent driving or reviewing the running app** understand what's on screen — *runtime behavior*, not source structure. Useful gut check: would this note show up usefully in the `[Guide]` of a snapshot the agent's about to interact with? If the answer requires holding the codebase in hand too, it belongs in source comments or `CLAUDE.md`, not here. + +**Good memory candidates:** stateful behavior (how toggles change the rendered UI), auth gates and credentials, form rules, multi-step interactions, runtime quirks. + +**Stay out of memory:** file paths, JSX/CSS patterns, style conventions, external doc references — all discoverable elsewhere or owned by other artifacts. + +Concrete — after editing a `Hero` component: + +```yaml +- "Audience toggle re-renders all H1 copy between 'builders' and 'agents'" # ✓ runtime +- "Headline copy lives in the `copy` object as JSX with both variants" # ✗ source structure +- "H1 emphasis uses " # ✗ implementation +- "Positioning doc at src/.../current.md retires 'sight' language" # ✗ external ref +``` + +## Views + +A view represents a screen or route in the application. Views provide: + +1. **"You are here" context** — the snapshot header identifies the current view by name +2. **Scoped component definitions** — components that only exist on certain views +3. **Metadata** — description, source file reference + +### Schema + +```yaml +version: 1 + +# Global components — matched on all views +components: + - name: NavBar + selector: "nav.main-nav" + children: + - name: nav-link + selector: "a.nav-link" + +# View definitions +views: + - name: HomePage + route: "/" + description: Main landing page + source: pages/Home.tsx + components: + - name: HeroSection + selector: ".hero-section" + children: + - name: hero-cta + selector: "button.cta" + + - name: ProductDetail + route: "/products/*" + description: Individual product page + source: pages/ProductDetail.tsx + components: + - name: ProductGallery + selector: ".product-gallery" + children: + - name: gallery-image + selector: ".gallery-img" + - name: AddToCartButton + selector: "button.add-to-cart" + + - name: UserSettings + route: "/users/*/settings" + source: pages/UserSettings.tsx +``` + +### View Fields + +- **name** (required): Semantic name shown in snapshot header +- **route** (required): Glob pattern matched against URL pathname +- **description** (optional): Brief description of the view +- **source** (optional): Relative path to the source file +- **components** (optional): View-scoped component definitions (same schema as global components). These are matched **in addition to** globals (additive model). + +### Route matching + +- `route` is a glob pattern matched against `new URL(pageUrl).pathname` +- `*` matches a single path segment (e.g., `/products/*` matches `/products/123`) +- `**` matches any depth (e.g., `/admin/**` matches `/admin/users/42/edit`) +- Exact routes work too: `/` matches only the root +- First matching view wins (definition order = priority) +- If no view matches, only global components are used + +### How scoped components work + +When a view matches, its `components` are merged with the top-level global components before matching against the DOM. This is additive — view components supplement globals, they don't replace them. + +For example, with the schema above on `/products/123`: +- Global `NavBar` and `nav-link` are matched (always) +- View-scoped `ProductGallery`, `gallery-image`, and `AddToCartButton` are also matched +- Total: all five component names are available in the snapshot + +## Requests + +Requests map API endpoints to semantic names with optional payload schemas. When matched, network tools ({{tool "live-net-list"}}, {{tool "live-net-get"}}) overlay the definition metadata — giving immediate context about what each request does. + +### Schema + +```yaml +version: 1 +requests: + - name: FetchFlights + route: "/api/flights" + method: GET + description: Search for available flights + source: src/api/flights.ts + request: + fields: + - name: origin + type: string + description: Origin airport code + - name: destination + type: string + description: Destination airport code + response: + fields: + - name: flights + type: array + description: List of available flights + - name: total + type: number + headers: + - Authorization + + - name: GetFlightFares + route: "/api/flights/:id/fares/:category" + method: GET + description: Fare options for a specific flight and cabin class + + - name: CreateBooking + route: "/api/bookings" + method: POST + description: Create a new flight booking + source: src/api/bookings.ts +``` + +### Request Fields + +- **name** (required): Semantic name shown in network tool output +- **route** (required): Glob pattern matched against the URL pathname. Express-style `:param` segments are converted to `*` before matching (e.g., `/api/flights/:id` becomes `/api/flights/*`). +- **method** (optional): HTTP method filter (e.g., `GET`, `POST`). If omitted, matches any method. +- **description** (optional): Brief description of what the endpoint does +- **source** (optional): Relative path to the source file implementing this endpoint +- **request** (optional): Expected request payload + - **fields**: Flat list of field descriptors (`name`, `type`, `description`) +- **response** (optional): Expected response payload (same structure as `request`) +- **headers** (optional): Notable header names to highlight + +### Route matching + +- `route` is a glob pattern matched against `new URL(requestUrl).pathname` +- Express-style `:param` segments are converted to `*` before matching +- `*` matches a single path segment: `/api/flights/*` matches `/api/flights/123` +- `**` matches any depth: `/api/**` matches `/api/v2/flights/123/fares` +- First matching definition wins (definition order = priority) + +### View-scoped requests + +Views can include a `requests` array, which is merged with global requests (additive, same pattern as view-scoped components): + +```yaml +version: 1 +requests: + - name: GetUser + route: "/api/user" + +views: + - name: SearchPage + route: "/search" + requests: + - name: SearchFlights + route: "/api/flights/search" + method: POST +``` + +When on `/search`, both `GetUser` and `SearchFlights` are available for matching. + +### Enriched Network Output + +**List view** ({{tool "live-net-list"}}) — matched requests show the semantic name: +``` +reqid=1 FetchFlights GET https://app.example.com/api/flights [success - 200] +reqid=2 GET https://app.example.com/assets/logo.png [success - 200] +``` + +**Detail view** ({{tool "live-net-get"}}) — a Sightmap section appears with description and field schemas: +``` +## Request https://app.example.com/api/flights +### Sightmap: FetchFlights +Search for available flights +Source: src/api/flights.ts +Expected request fields: +- origin (string) — Origin airport code +- destination (string) — Destination airport code +Expected response fields: +- flights (array) — List of available flights +- total (number) +Status: [success - 200] +### Request Headers +... +``` + +Unmatched requests render exactly as before — enrichment is non-fatal. + +## Enriched Snapshot Output + +With a matched view: + +``` +[View: ProductDetail "https://mystore.com/products/123"] + +uid=1_0 RootWebArea "Blue Widget - MyStore" + uid=1_1 NavBar visible interactive + uid=1_4 nav-link "Home" visible interactive + uid=1_6 nav-link "Products" visible interactive + uid=1_10 main visible + uid=1_15 ProductGallery visible + uid=1_16 gallery-image "Blue Widget front" visible + uid=1_17 gallery-image "Blue Widget side" visible + uid=1_20 AddToCartButton "Add to Cart" visible interactive +``` + +Without a matched view (globals only): + +``` +[Components: NavBar, nav-link] + +uid=1_0 RootWebArea "Some Page" + uid=1_1 NavBar visible interactive + uid=1_4 nav-link "Home" visible interactive + ... +``` + +Without definitions, elements still get `visible`/`interactive` annotations from the DOM probe but use generic a11y roles. + +## Using Sightmap in Snapshots + +When sightmap definitions are loaded, snapshots automatically annotate matched elements with semantic names. Component `memory` entries appear in a Component Guide section at the top of snapshot output, giving agents context about each component's purpose. The `source` and `description` fields are not uploaded but are available to agents reading `.sightmap/` files directly for local navigation and context. diff --git a/templates/skills/tunnel/SKILL.cli.md b/templates/skills/tunnel/SKILL.cli.md new file mode 100644 index 00000000..ea5053c0 --- /dev/null +++ b/templates/skills/tunnel/SKILL.cli.md @@ -0,0 +1,119 @@ +# Tunnel Setup for Hosted Browser + +When the hosted browser needs to load a page from the user's local dev server (e.g. `http://localhost:3000`), a reverse tunnel is required. The hosted browser cannot reach localhost directly — the tunnel proxies requests from the hosted infrastructure back to the user's machine. + +## Commands + +| Command | Description | +|---------|-------------| +| {{tool "live-tunnel"}} | Allocate a connection and get a relay URL for tunneling | +| {{tool "tunnel-connect"}} | Connect local server(s) to relay | +| {{tool "tunnel-status"}} | Check tunnel connection state | + +## When to Use + +- {{tool "live-connect"}} is called with a `localhost`, `127.0.0.1`, or other local URL +- The user asks to screenshot, test, or interact with their local dev server using hosted browser tools + +## The allowlist model + +{{tool "tunnel-connect"}} registers the tunnel with an **`allowedOrigins`** list. Every request that flows through the proxy is matched against the list; anything off-list is refused with a 502 (`ERR_TUNNEL_CONNECTION_FAILED` from chromium's perspective). This is the security boundary — without it, a buggy or hostile relay could probe arbitrary localhost services on the user's machine. + +**Grammar: `host:port`. No scheme. Subdomains are implicit.** + +- Each entry is a bare `host:port` — for example `example.test:8043` or `localhost:3000`. +- For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `example.test:8043` covers `app.example.test:8043`, `oauthtest.example.test:8043`, and so on. +- Hosts are restricted to the loopback class: `localhost`, `127.x`, `::1`, `*.test`, `*.localhost`. +- IP literals (`127.0.0.1:3000`, `[::1]:443`) match exactly with no subdomain expansion. +- Scheme is not part of the grammar; the same entry covers `http://` and `https://` on that `host:port`. + +The response from {{tool "tunnel-connect"}} may include a `canonicalized` field if your inputs were rewritten: + +```json +"canonicalized": [ + {"input": "www.example.test:8043", "canonical": "example.test:8043"} +] +``` + +Treat this as a soft warning: the relay accepted your entry but registered it as the canonical form. Use the canonical form in future calls. The parser also tolerates legacy `scheme://...` and `*.host:port` inputs for compatibility — both get canonicalized away. + +Default deny: omit something and chromium can't reach it through this tunnel. + +## Two Flows + +### Tunnel-first (recommended for localhost URLs) + +Set up the tunnel before opening a view. {{tool "live-tunnel"}} allocates the browser connection and returns a `connectionId` — use it with {{tool "live-view-new"}} to navigate. + +1. Run {{tool "live-tunnel"}} → returns `relayUrl`, `connectionId`, and `sightmapUploadUrl` +2. If the project has `.sightmap/` definitions, upload them now (see `subtext:shared`). Upload before {{tool "live-view-new"}} so the sightmap is active for the first snapshot. +3. Run {{tool "tunnel-connect"}} with `relayUrl` and `allowedOrigins` +4. Verify `state` is `"ready"` in the response +5. Run {{tool "live-view-new"}} with the `connection_id` from step 1 and the full localhost URL + +``` +subtext live tunnel → { relayUrl, connectionId: "abc-123", sightmapUploadUrl: "..." } +# upload .sightmap/ here if project has definitions (see subtext:shared) +subtext tunnel connect --relay-url --allowed-origins localhost:3000 +→ { state: "ready", tunnelId: "..." } +subtext live view-new --connection-id abc-123 --url http://localhost:3000/dashboard +``` + +### Connection-first (attach tunnel to existing connection) + +If {{tool "live-connect"}} was already called and you need to attach a tunnel afterward, pass the existing `connectionId` to {{tool "live-tunnel"}}. + +1. Run {{tool "live-tunnel"}} with `--connection-id` from the existing connection → returns `relayUrl` +2. Run {{tool "tunnel-connect"}} with `relayUrl` and `allowedOrigins` +3. Verify `state` is `"ready"` in the response +4. Navigate to the localhost URL with {{tool "live-view-navigate"}} + +## Picking an allowlist + +> **Default: list the trunk, not the subdomain you happen to be navigating to.** OAuth/SSO redirects will bounce out of any narrower entry within seconds of login, and chromium lands on `chrome-error://chromewebdata/` when that happens. The bare trunk implicitly covers every subdomain on the same port. + +- **App with auth/SSO redirects between subdomains** (the common case). List the trunk: + ``` + --allowed-origins example.test:8043 + ``` + This covers `app.example.test:8043`, `oauthtest.example.test:8043`, every other subdomain. Don't narrow to `app.example.test:8043` — the first OAuth bounce will fail. + +- **Multi-port local stack** (web app on `:3000` + API on `:4200`) — list each origin: + ``` + --allowed-origins localhost:3000,localhost:4200 + ``` + +- **Single-page local app, one origin, no auth** — bare trunk works: + ``` + --allowed-origins localhost:3000 + ``` + +## Diagnosing a chrome-error page + +Symptom: chromium lands on `chrome-error://chromewebdata/` (visible in {{tool "live-view-screenshot"}} or as a blank page after a navigation/click). + +Likely cause: an allowlist miss on a redirect — the navigation went somewhere not on `allowedOrigins` and the tunnel refused it. OAuth and SSO logins are the dominant trigger. + +Recovery (do this; don't keep navigating): + +1. {{tool "tunnel-disconnect"}} the current tunnel. +2. {{tool "live-tunnel"}} again — the `connection_id` is preserved across reconnect, so chromium continuity is fine. +3. {{tool "tunnel-connect"}} with a trunk that covers the redirect target (e.g. `example.test:8043` instead of `app.example.test:8043`). +4. Retry the navigation that failed. + +If the trunk reconnect still fails the same way, the navigation is going somewhere outside that trunk entirely (different domain, different port). Widen further or ask a human. + +## Common mistakes + +- **Don't use {{tool "live-connect"}} for localhost / local URLs.** It mints its own connection ID and can't bind to a tunnel — use the tunnel-first flow ({{tool "live-tunnel"}} → {{tool "tunnel-connect"}} → {{tool "live-view-new"}}) instead. +- **Don't narrow the allowlist to a specific subdomain.** Login flows redirect; the navigation target is rarely the only origin you'll need. Default to the trunk. +- **Don't include `https://` or `*.` in entries.** The parser strips them for compatibility, but the canonical form is just `host:port`. +- **Don't open multiple tunnels per connection.** A single tunnel carries many origins — widen the allowlist instead. + +## Notes + +- **Never fabricate a `connectionId`** — only use IDs returned from {{tool "live-connect"}}, {{tool "live-tunnel"}}, or {{tool "tunnel-connect"}} calls. +- {{tool "live-tunnel"}} allocates a browser connection on the same pod as the tunnel relay. In tunnel-first flow, this replaces {{tool "live-connect"}} — use {{tool "live-view-new"}} to open views instead. +- The tunnel stays connected across multiple views — you only need to set it up once per connection. +- If the tunnel disconnects (e.g. the relay restarts), it reconnects automatically. Run {{tool "tunnel-status"}} to check. +- The tunnel only needs to be set up for localhost/local URLs. Remote URLs (e.g. `https://example.com`) work directly without a tunnel. diff --git a/templates/skills/tunnel/SKILL.md b/templates/skills/tunnel/SKILL.md new file mode 100644 index 00000000..7007f3cb --- /dev/null +++ b/templates/skills/tunnel/SKILL.md @@ -0,0 +1,153 @@ +--- +name: tunnel +description: Use when opening a hosted browser connection against a localhost or local dev server URL. Sets up a reverse tunnel so the hosted browser can reach the user's local server. +metadata: + targets: [mcp, cli] + requires: + skills: ["subtext:shared", "subtext:live"] +--- + +# Tunnel Setup for Hosted Browser + +> **ENVIRONMENT:** If a `subtext-environment` skill is available in the host project, read it before connecting — it specifies which MCP server prefix to use for live and tunnel tools. + +When the hosted browser needs to load a page from the user's local dev server (e.g. `http://localhost:3000`), a reverse tunnel is required. The hosted browser cannot reach localhost directly — the tunnel proxies requests from the hosted infrastructure back to the user's machine. + +{{if eq .Target "cli"}}## Commands{{else}}## MCP Tools{{end}} + +| Tool | Server | Description | +|------|--------|-------------| +| {{tool "live-tunnel"}} | subtext | Allocate a connection and get a relay URL for tunneling | +| {{tool "tunnel-connect"}} | subtext-tunnel | Connect local server(s) to relay | +| {{tool "tunnel-status"}} | subtext-tunnel | Check tunnel connection state | + +## When to Use + +- {{tool "live-connect"}} is called with a `localhost`, `127.0.0.1`, or other local URL +- The user asks to screenshot, test, or interact with their local dev server using hosted browser tools + +## The allowlist model + +{{tool "tunnel-connect"}} registers the tunnel with an **`allowedOrigins`** list. Every request that flows through the proxy is matched against the list; anything off-list is refused with a 502 (`ERR_TUNNEL_CONNECTION_FAILED` from chromium's perspective). This is the security boundary — without it, a buggy or hostile relay could probe arbitrary localhost services on the user's machine. + +**Grammar: `host:port`. No scheme. Subdomains are implicit.** + +- Each entry is a bare `host:port` — for example `example.test:8043` or `localhost:3000`. +- For DNS hosts, the entry matches the bare host **and any subdomain on the same port**. List the trunk you want to allow, not individual subdomains: `example.test:8043` covers `app.example.test:8043`, `oauthtest.example.test:8043`, and so on. +- Hosts are restricted to the loopback class: `localhost`, `127.x`, `::1`, `*.test`, `*.localhost`. +- IP literals (`127.0.0.1:3000`, `[::1]:443`) match exactly with no subdomain expansion. +- Scheme is not part of the grammar; the same entry covers `http://` and `https://` on that `host:port`. + +The response from {{tool "tunnel-connect"}} may include a `canonicalized` field if your inputs were rewritten: + +```json +"canonicalized": [ + {"input": "www.example.test:8043", "canonical": "example.test:8043"} +] +``` + +Treat this as a soft warning: the relay accepted your entry but registered it as the canonical form. Use the canonical form in future calls. The parser also tolerates legacy `scheme://...` and `*.host:port` inputs for compatibility — both get canonicalized away. + +Default deny: omit something and chromium can't reach it through this tunnel. + +## Two Flows + +### Tunnel-first (recommended for localhost URLs) + +Set up the tunnel before opening a view. {{tool "live-tunnel"}} allocates the browser connection and returns a `connectionId` — use it with {{tool "live-view-new"}} to navigate. + +1. Call {{tool "live-tunnel"}} on the **subtext** MCP server → returns `relayUrl`, `connectionId`, and `sightmapUploadUrl` +2. If the project has `.sightmap/` definitions, upload them now (see `subtext:shared`). Upload before {{tool "live-view-new"}} so the sightmap is active for the first snapshot. +3. Call {{tool "tunnel-connect"}} on the **subtext-tunnel** MCP server with `relayUrl` and `allowedOrigins` +4. Verify `state` is `"ready"` in the response +5. Call {{tool "live-view-new"}} on **subtext** with the `connection_id` from step 1 and the full localhost URL + +``` +live-tunnel() → { relayUrl, connectionId: "abc-123", sightmapUploadUrl: "..." } +# upload .sightmap/ here if project has definitions (see subtext:shared) +tunnel-connect({ + relayUrl, + allowedOrigins: ["localhost:3000"], +}) → { state: "ready", tunnelId: "..." } +live-view-new({ connection_id: "abc-123", url: "http://localhost:3000/dashboard" }) +``` + +### Connection-first (attach tunnel to existing connection) + +If {{tool "live-connect"}} was already called and you need to attach a tunnel afterward, pass the existing `connectionId` to {{tool "live-tunnel"}}. + +1. Call {{tool "live-tunnel"}} on the **subtext** MCP server with `connection_id` from the existing connection → returns `relayUrl` +2. Call {{tool "tunnel-connect"}} on the **subtext-tunnel** MCP server with `relayUrl` and `allowedOrigins` +3. Verify `state` is `"ready"` in the response +4. Navigate to the localhost URL with {{tool "live-view-navigate"}} + +``` +live-tunnel({ connection_id: "existing-conn-id" }) → { relayUrl, connectionId: "existing-conn-id" } +tunnel-connect({ + relayUrl, + allowedOrigins: ["localhost:3000"], +}) → { state: "ready", tunnelId: "..." } +live-view-navigate({ connection_id: "existing-conn-id", url: "http://localhost:3000" }) +``` + +## Picking an allowlist + +> **Default: list the trunk, not the subdomain you happen to be navigating to.** OAuth/SSO redirects will bounce out of any narrower entry within seconds of login, and chromium lands on `chrome-error://chromewebdata/` when that happens. The bare trunk implicitly covers every subdomain on the same port. + +- **App with auth/SSO redirects between subdomains** (the common case). List the trunk: + ``` + allowedOrigins: ["example.test:8043"] + ``` + This covers `app.example.test:8043`, `oauthtest.example.test:8043`, every other subdomain. Don't narrow to `app.example.test:8043` — the first OAuth bounce will fail. + +- **Multi-port local stack** (web app on `:3000` + API on `:4200`, frontend + asset server, etc.) — list each origin: + ``` + allowedOrigins: [ + "localhost:3000", + "localhost:4200", + ] + ``` + +- **Single-page local app, one origin, no auth** — bare trunk works: + ``` + allowedOrigins: ["localhost:3000"] + ``` + (Subdomains of `localhost` would also match. That's fine — they all resolve to your loopback interface anyway.) + +- **Mixed hosts** — combine freely in one tunnel: + ``` + allowedOrigins: [ + "example.test:8043", + "127.0.0.1:8766", + ] + ``` + +## Diagnosing a chrome-error page + +Symptom: chromium lands on `chrome-error://chromewebdata/` (visible in {{tool "live-view-screenshot"}} or as a blank page after a navigation/click). + +Likely cause: an allowlist miss on a redirect — the navigation went somewhere not on `allowedOrigins` and the tunnel refused it. OAuth and SSO logins are the dominant trigger. + +Recovery (do this; don't keep navigating): + +1. {{tool "tunnel-disconnect"}} the current tunnel. +2. {{tool "live-tunnel"}} again — the `connection_id` is preserved across reconnect, so chromium continuity is fine. +3. {{tool "tunnel-connect"}} with a trunk that covers the redirect target (e.g. `example.test:8043` instead of `app.example.test:8043`). +4. Retry the navigation that failed. + +If the trunk reconnect still fails the same way, the navigation is going somewhere outside that trunk entirely (different domain, different port). Widen further or ask a human. + +## Common mistakes + +- **Don't use {{tool "live-connect"}} for localhost / local URLs.** It mints its own connection ID and can't bind to a tunnel — use the tunnel-first flow ({{tool "live-tunnel"}} → {{tool "tunnel-connect"}} → {{tool "live-view-new"}}) instead. +- **Don't narrow the allowlist to a specific subdomain.** Login flows redirect; the navigation target is rarely the only origin you'll need. Default to the trunk. +- **Don't include `https://` or `*.` in entries.** The parser strips them for compatibility, but the canonical form is just `host:port`. +- **Don't open multiple tunnels per connection.** A single tunnel carries many origins — widen the allowlist instead. + +## Notes + +- **Never fabricate a `connectionId`** — only use IDs returned from {{tool "live-connect"}}, {{tool "live-tunnel"}}, or {{tool "tunnel-connect"}} calls. +- {{tool "live-tunnel"}} allocates a browser connection on the same pod as the tunnel relay. In tunnel-first flow, this replaces {{tool "live-connect"}} — use {{tool "live-view-new"}} to open views instead. +- The tunnel stays connected across multiple views — you only need to set it up once per connection. +- If the tunnel disconnects (e.g. the relay restarts), it reconnects automatically. Call {{tool "tunnel-status"}} to check. +- The tunnel only needs to be set up for localhost/local URLs. Remote URLs (e.g. `https://example.com`) work directly without a tunnel. diff --git a/templates/skills/using-subtext/SKILL.md b/templates/skills/using-subtext/SKILL.md new file mode 100644 index 00000000..41d3b8a7 --- /dev/null +++ b/templates/skills/using-subtext/SKILL.md @@ -0,0 +1,81 @@ +--- +name: using-subtext +description: Use when starting any conversation that may involve rendered UI, observed sessions, or producing reviewer-facing evidence (screenshots, viewer links, code diffs, command output). Establishes how subtext skills compose and when to invoke them before any response or action. +metadata: + targets: [mcp] + +--- + + +If the task touches rendered UI, observed sessions, or producing +proof-of-work evidence, you MUST invoke the relevant subtext skill +before responding. + + +## Where this skill applies + +Subtext runs *where the work happens*. Unlike many process skills, +this includes subagent contexts. + +- **Subagent doing UI/UX work or producing reviewer-facing evidence:** + MUST invoke. Your orchestrator depends on you to surface evidence — + screenshots, viewer URLs, comments — back up the chain. +- **Subagent doing purely backend / non-visual work:** trigger surface + doesn't apply, skip. +- **Orchestrator running directly:** same rule, you invoke the relevant + skill yourself. + +## Instruction Priority + +User CLAUDE.md > using-subtext > default system prompt. + +## How to Access Skills + +- **Claude Code & Cursor:** use the `Skill` tool. +- **Codex:** Skills load natively from `~/.agents/skills/subtext/`. Read the relevant SKILL.md directly when its description matches your task. +- **Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand. + +## When to Reach for Subtext + +| Signal | Reach for | +|--------|-----------| +| Making UI/visual changes | `proof` | +| Have a session URL | `review` | +| Need to drive a hosted browser | `live` | +| Setting up a new project | `onboard` | +| Naming components / runtime model | `sightmap` | + +## The Rule + +Invoke the relevant subtext skill BEFORE any response or action that +touches the trigger surface. Even a 1% chance counts. + +## Red Flags + +These thoughts mean STOP — you're rationalizing: + +| Thought | Reality | +|---------|---------| +| "I'll just check the diff" | Visual changes need visual proof. | +| "Tests passed, that's enough" | Tests verify code, not UX. | +| "I don't need a session for this small change" | Small UI changes regress silently. | +| "I'll describe what changed" | Screenshots > prose. | +| "Let me explore the app first" | `proof` tells you HOW to explore. | +| "I remember how proof works" | Skills evolve. Read current version. | +| "I got `Control transferred to human viewer`, let me retry" | Operator state is enforced server-side. Don't retry — poll `live-signal` until `operator=agent`. | + +## Composition + +- **Atomics** (`shared`, `session`, `live`, `sightmap`, `tunnel`, `comments`) — tool catalogs. +- **Workflows** (`proof`, `review`) — orchestration. `proof` is the inner loop, `review` is the outer loop. +- **Recipes** (`recipe-sightmap-setup`) — short step lists. +- **Onboarding** (`onboard`, `setup-plugin`, `first-session`) — first-time user setup. + +``` +proof ──▶ session recorded ──▶ review (optional handoff) +``` + +## Skill Types + +- **Rigid** (`proof`): follow exactly. +- **Flexible** (atomics): adapt to context.