Explorar el Código

fix(opencode): align V2 tool mapping with the 2.0.3 catalog; harden plugin internals

- V2 bootstrap mapping now teaches write/edit/websearch (verified against
  a live v2.0.3 /api/plugin tool catalog) instead of routing all file
  mutation through patch
- frontmatter parser tolerates CRLF and YAML block scalars/continuation
  lines
- child-session cache is bounded (512 entries, oldest-quarter eviction)
- INSTALL.md and docs/README.opencode.md tool tables synced to the 2.0.3
  catalog; unit-test needle list extended to cover the new tools
GoldJohnKing hace 2 semanas
padre
commit
4996eb00bd

+ 4 - 1
.opencode/INSTALL.md

@@ -117,10 +117,13 @@ Skills speak in actions ("create a todo", "dispatch a subagent", "read a file").
 - `Subagent (general-purpose):` template → `subagent` tool with `agent: "general"` (or `"explore"`); pass `sessionID` to continue a previous subagent
 - "Invoke a skill" → OpenCode's native `skill` tool
 - "Read a file" → `read`
-- "Create a file" / "edit a file" / "delete a file" → `patch` (same patch format, via `patchText`)
+- "Create / overwrite a file" → `write`
+- "Edit a file" → `edit` for targeted changes, or `patch` (same patch format, via `patchText`) when a skill speaks in patch format
+- "Delete a file" → `patch` (via `patchText`) or a `shell` `rm`
 - "Run a shell command" → `shell` (`command`, `workdir`, `timeout`, `background`)
 - "Search file contents" / "find files by name" → `grep`, `glob`
 - "Fetch a URL" → `webfetch`
+- "Search the web" → `websearch`
 
 ## Getting Help
 

+ 44 - 10
.opencode/plugins/superpowers.js

@@ -24,21 +24,35 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
 // Skills directory shared by V1 (config hook) and V2 (setup/ctx.skill.transform)
 const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
 
-// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
+// Simple frontmatter extraction (avoid dependency on skills-core for
+// bootstrap). Handles plain `key: value` lines, quoted values, YAML block
+// scalar markers (`>`, `|`) with indented continuation lines, and CRLF line
+// endings. Not a full YAML parser — nested maps flatten into their parent
+// key's value, which is fine for the name/description fields consumed here.
 const extractAndStripFrontmatter = (content) => {
-  const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
+  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
   if (!match) return { frontmatter: {}, content };
 
   const frontmatterStr = match[1];
   const body = match[2];
   const frontmatter = {};
+  let lastKey = null;
 
-  for (const line of frontmatterStr.split('\n')) {
+  for (const rawLine of frontmatterStr.split('\n')) {
+    const line = rawLine.replace(/\r$/, '');
     const colonIdx = line.indexOf(':');
-    if (colonIdx > 0) {
+    if (colonIdx > 0 && !/^\s/.test(line)) {
       const key = line.slice(0, colonIdx).trim();
       const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
-      frontmatter[key] = value;
+      // Block scalar markers (>, |, optionally with +/- chomping) carry no
+      // value themselves; the indented lines that follow do.
+      frontmatter[key] = /^(>[+-]?|\|[+-]?)$/.test(value) ? '' : value;
+      lastKey = key;
+    } else if (lastKey !== null && line.trim() !== '') {
+      // Continuation of a multi-line value: append rather than drop so long
+      // descriptions survive parsing. Newlines collapse to spaces — good
+      // enough for the single-line name/description fields consumed here.
+      frontmatter[lastKey] = `${frontmatter[lastKey]} ${line.trim()}`.trim();
     }
   }
 
@@ -67,17 +81,21 @@ Use OpenCode's native \`skill\` tool to list and load skills.`;
 
 // V2 built-ins: no todo tool at all; task → subagent (agent name in 'agent',
 // continuation via sessionID); apply_patch → patch (patchText, same patch
-// format); bash → shell. read, grep, glob, webfetch, skill keep their names.
+// format); bash → shell. read, write, edit, grep, glob, webfetch, websearch,
+// and skill all exist under those names (verified against the 2.0.3 tool
+// catalog served by /api/plugin).
 export const V2_MAPPING = `**Tool Mapping for OpenCode:**
 When skills request actions, substitute OpenCode equivalents:
 - Create or update todos → OpenCode v2 has no todo tool; track the plan in a markdown file (or the harness's plan facility) instead
 - \`Subagent (general-purpose):\` → \`subagent\` with \`agent: "general"\` (give it \`description\` and \`prompt\`, optionally \`background\`; pass \`sessionID\` to continue a previous subagent)
 - Invoke a skill → OpenCode's native \`skill\` tool
 - Read files → \`read\`
-- Create, edit, or delete files → \`patch\` with \`patchText\` (same patch format)
+- Create or overwrite a file → \`write\`
+- Edit files → \`edit\` for targeted changes, or \`patch\` with \`patchText\` (same patch format) when a skill speaks in patch format or deletes files
 - Run shell commands → \`shell\` (\`command\`, \`workdir\`, \`timeout\`, \`background\`)
 - Search files → \`grep\`, \`glob\`
 - Fetch a URL → \`webfetch\`
+- Search the web → \`websearch\`
 
 Use OpenCode's native \`skill\` tool to list and load skills.`;
 
@@ -129,10 +147,26 @@ ${toolMapping}
 // execution skills.
 
 // sessionID -> is-child decision. parentID never changes for a session, so
-// the result is cached for life and the injection hook (which fires on every
-// agent step) pays only one client roundtrip per session.
+// the result is cached until eviction and the injection hook (which fires on
+// every agent step) pays only one client roundtrip per session. The V2
+// service process is long-lived and sessions accumulate over weeks, so the
+// cache is bounded: when full, drop the oldest quarter (Map iterates keys in
+// insertion order). An evicted session merely pays one extra lookup if seen
+// again.
+const CHILD_SESSION_CACHE_MAX = 512;
 const _childSessionCache = new Map();
 
+const _cacheChildSession = (sessionID, isChild) => {
+  if (_childSessionCache.size >= CHILD_SESSION_CACHE_MAX) {
+    let toDrop = Math.ceil(CHILD_SESSION_CACHE_MAX / 4);
+    for (const key of _childSessionCache.keys()) {
+      if (toDrop-- <= 0) break;
+      _childSessionCache.delete(key);
+    }
+  }
+  _childSessionCache.set(sessionID, isChild);
+};
+
 const isChildSession = async (fetchSession, sessionID) => {
   if (!sessionID) return false; // unknown session: keep current behavior
   if (_childSessionCache.has(sessionID)) return _childSessionCache.get(sessionID);
@@ -155,7 +189,7 @@ const isChildSession = async (fetchSession, sessionID) => {
     console.error('[superpowers] session lookup failed, treating session as top-level:', err);
     return false;
   }
-  _childSessionCache.set(sessionID, isChild);
+  _cacheChildSession(sessionID, isChild);
   return isChild;
 };
 

+ 6 - 3
docs/README.opencode.md

@@ -126,14 +126,17 @@ Skills speak in actions rather than naming any one runtime's tools. The bootstra
 - `Subagent (general-purpose):` template → OpenCode's `subagent` tool with `agent: "general"` (or `"explore"`); pass `sessionID` to continue a previous subagent
 - "Invoke a skill" → OpenCode's native `skill` tool
 - "Read a file" → `read`
-- "Create a file" / "edit a file" / "delete a file" → `patch` with `patchText` (same patch format as V1's `apply_patch`)
+- "Create / overwrite a file" → `write`
+- "Edit a file" → `edit` for targeted changes, or `patch` with `patchText` (same patch format as V1's `apply_patch`) when a skill speaks in patch format
+- "Delete a file" → `patch` (via `patchText`) or a `shell` `rm`
 - "Run a shell command" → `shell` (`command`, `workdir`, `timeout`, `background`)
 - "Search file contents" / "find files by name" → `grep`, `glob`
 - "Fetch a URL" → `webfetch`
+- "Search the web" → `websearch`
 
-In short, V2 renamed `task` → `subagent` (the agent name moved from `subagent_type` to `agent`, and continuation happens by re-invoking with `sessionID`), `apply_patch` → `patch`, and `bash` → `shell`, and it dropped the todo tool entirely; `read`, `grep`, `glob`, `webfetch`, and `skill` keep their V1 names.
+In short, V2 renamed `task` → `subagent` (the agent name moved from `subagent_type` to `agent`, and continuation happens by re-invoking with `sessionID`), `apply_patch` → `patch`, and `bash` → `shell`, and it dropped the todo tool entirely; `read`, `write`, `edit`, `grep`, `glob`, `webfetch`, `websearch`, and `skill` keep their V1 names (`write`/`edit`/`websearch` replace V1's `apply_patch`-only path, `apply_patch`-only mutation habits, and V1's lack of a search tool respectively).
 
-(V1 list verified against the installed OpenCode 1.18.x CLI's tool inventory; V2 list verified against the V2 source at `dbd9b18`.)
+(V1 list verified against the installed OpenCode 1.18.x CLI's tool inventory; V2 list verified against the 2.0.3 tool catalog served by a live v2.0.3 service via `/api/plugin`.)
 
 ## Troubleshooting
 

+ 3 - 0
tests/opencode/test-bootstrap-caching.mjs

@@ -181,11 +181,14 @@ function assertMappingConstants(mod) {
     '`subagent` with `agent: "general"`',
     '`sessionID` to continue a previous subagent',
     'no todo tool',
+    '`write`',
+    '`edit`',
     '`patch` with `patchText`',
     '`shell`',
     '`read`',
     '`grep`, `glob`',
     '`webfetch`',
+    '`websearch`',
   ]) {
     if (!mod.V2_MAPPING.includes(needle)) {
       failures.push(`expected V2_MAPPING to teach the V2 tool ${needle}`);