ofiere-openclaw-plugin 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "3.4.0",
3
+ "version": "3.5.1",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM 9 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, and prompts",
5
+ "description": "OpenClaw plugin for Ofiere PM - 10 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, and constellation agent architecture",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -70,6 +70,19 @@ const TOOL_DOCS: Record<string, string> = {
70
70
  - update: Change name, content, color, category, or order
71
71
  - delete: Remove a chunk by ID
72
72
  - All modifications are logged for audit`,
73
+
74
+ OFIERE_CONSTELLATION_OPS: `- **OFIERE_CONSTELLATION_OPS** — Create, edit, delete, and manage OpenClaw agent architectures from chat (action: "list_agents", "get_agent", "read_file", "write_file", "create_agent", "delete_agent", "delete_file", "read_blueprint", "list_agent_mesh")
75
+ - CRITICAL: Before creating or editing any agent, ALWAYS call read_blueprint first
76
+ - list_agents: See all agents with codename, role, and file list
77
+ - get_agent: Full details for one agent including file sizes and identity
78
+ - read_file / write_file: Read or overwrite any agent markdown file (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, etc.)
79
+ - create_agent: Scaffold a new agent with structured params (name, codename, role, emoji, mission, personality, etc.). Auto-registers in OpenClaw
80
+ - delete_agent: ⚠️ PERMANENTLY delete an entire agent workspace and unregister. Requires confirm: true. ALWAYS ask user for explicit confirmation first — this is IRREVERSIBLE
81
+ - delete_file: Remove a specific file from an agent workspace
82
+ - read_blueprint: Read the OpenClaw Agent Blueprint — the canonical structure reference
83
+ - list_agent_mesh: See the sovereignty map (which agent owns what domain)
84
+ - New agents get workspace-<name>/ with IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, <CODENAME>.md, and skills/
85
+ - All changes sync to the Constellation dashboard automatically`,
73
86
  };
74
87
 
75
88
  export function getSystemPrompt(state: {
@@ -114,6 +127,9 @@ ${toolDocs}
114
127
  - When creating dependencies, use OFIERE_PROJECT_OPS to link predecessor/successor tasks.
115
128
  - Prompt chunk modifications (OFIERE_PROMPT_OPS) are powerful — use thoughtfully as they change agent behavior.
116
129
  - When the user asks about "knowledge base", "knowledge library", "knowledge entries", or wants to recall stored knowledge, ALWAYS use OFIERE_KNOWLEDGE_OPS — do NOT rely on your own memory for this.
130
+ - When creating or editing an agent's architecture, ALWAYS use OFIERE_CONSTELLATION_OPS action:"read_blueprint" first to understand the required structure.
131
+ - When creating a new agent, use OFIERE_CONSTELLATION_OPS action:"create_agent" with all available structured params. The agent will be auto-registered in OpenClaw.
132
+ - When deleting an agent, ALWAYS ask the user for explicit confirmation BEFORE calling OFIERE_CONSTELLATION_OPS action:"delete_agent" with confirm: true. Show them what will be deleted. This action is IRREVERSIBLE.
117
133
  </ofiere-pm>`;
118
134
  }
119
135
 
package/src/tools.ts CHANGED
@@ -547,6 +547,18 @@ async function handleUpdateTask(
547
547
  }
548
548
  if (params.status === "DONE") updates.completed_at = new Date().toISOString();
549
549
 
550
+ // If task is being marked DONE or FAILED, auto-complete any linked scheduler events
551
+ if (params.status === "DONE" || params.status === "FAILED") {
552
+ try {
553
+ await supabase
554
+ .from("scheduler_events")
555
+ .update({ status: "completed", next_run_at: null, updated_at: new Date().toISOString() })
556
+ .eq("task_id", params.task_id as string);
557
+ } catch (_schedErr) {
558
+ // Non-fatal: task update should still proceed
559
+ }
560
+ }
561
+
550
562
  // Handle custom_fields updates (execution_plan, goals, constraints, system_prompt, instructions)
551
563
  const hasCustomFields = params.execution_plan !== undefined ||
552
564
  params.goals !== undefined ||
@@ -1673,6 +1685,628 @@ function registerPromptOps(
1673
1685
  });
1674
1686
  }
1675
1687
 
1688
+ // ═══════════════════════════════════════════════════════════════════════════════
1689
+ // META-TOOL 10: OFIERE_CONSTELLATION_OPS — Agent Architecture Builder
1690
+ // ═══════════════════════════════════════════════════════════════════════════════
1691
+
1692
+ function registerConstellationOps(
1693
+ api: any,
1694
+ supabase: SupabaseClient,
1695
+ userId: string,
1696
+ ): void {
1697
+ // Resolve the OpenClaw data directory — the plugin runs inside the gateway process
1698
+ const fs = require("fs");
1699
+ const path = require("path");
1700
+ const { execSync } = require("child_process");
1701
+
1702
+ // OpenClaw stores data at /data/.openclaw/ inside the Docker container
1703
+ // Fallback: check HOME/.openclaw/ for local dev
1704
+ const OPENCLAW_ROOT =
1705
+ fs.existsSync("/data/.openclaw") ? "/data/.openclaw"
1706
+ : path.join(process.env.HOME || process.env.USERPROFILE || "/root", ".openclaw");
1707
+
1708
+ // ── Helpers ──
1709
+
1710
+ function getWorkspacePath(agentName: string): string {
1711
+ return path.join(OPENCLAW_ROOT, `workspace-${agentName.toLowerCase()}`);
1712
+ }
1713
+
1714
+ function listWorkspaceAgents(): Array<{ name: string; codename: string; role: string; files: string[]; hasSkills: boolean }> {
1715
+ const result: Array<{ name: string; codename: string; role: string; files: string[]; hasSkills: boolean }> = [];
1716
+ try {
1717
+ const entries = fs.readdirSync(OPENCLAW_ROOT, { withFileTypes: true });
1718
+ for (const entry of entries) {
1719
+ if (!entry.isDirectory() || !entry.name.startsWith("workspace-")) continue;
1720
+ const agentName = entry.name.replace("workspace-", "");
1721
+ if (agentName === "main" || agentName === "zero" || agentName === "echo") continue; // skip system agents
1722
+ const wsPath = path.join(OPENCLAW_ROOT, entry.name);
1723
+ const files = fs.readdirSync(wsPath).filter((f: string) => f.endsWith(".md"));
1724
+ // Try to extract codename and role from IDENTITY.md
1725
+ let codename = agentName.toUpperCase();
1726
+ let role = "Agent";
1727
+ try {
1728
+ const idContent = fs.readFileSync(path.join(wsPath, "IDENTITY.md"), "utf8");
1729
+ const cnMatch = idContent.match(/\*\*Codename[\s:]*\*\*[\s:]*(.+)/i);
1730
+ if (cnMatch) codename = cnMatch[1].trim();
1731
+ const roleMatch = idContent.match(/\*\*Role[\s:]*\*\*[\s:]*(.+)/i);
1732
+ if (roleMatch) role = roleMatch[1].trim();
1733
+ } catch {}
1734
+ const hasSkills = fs.existsSync(path.join(wsPath, "skills"));
1735
+ result.push({ name: agentName, codename, role, files, hasSkills });
1736
+ }
1737
+ } catch (e) {
1738
+ api.logger?.warn?.(`[ofiere] Failed to list workspaces: ${e}`);
1739
+ }
1740
+ return result;
1741
+ }
1742
+
1743
+ function readAgentFile(agentName: string, fileName: string): string | null {
1744
+ try {
1745
+ const filePath = path.join(getWorkspacePath(agentName), fileName);
1746
+ return fs.readFileSync(filePath, "utf8");
1747
+ } catch { return null; }
1748
+ }
1749
+
1750
+ function writeAgentFile(agentName: string, fileName: string, content: string): void {
1751
+ const wsPath = getWorkspacePath(agentName);
1752
+ if (!fs.existsSync(wsPath)) fs.mkdirSync(wsPath, { recursive: true, mode: 0o777 });
1753
+ fs.writeFileSync(path.join(wsPath, fileName), content, "utf8");
1754
+ }
1755
+
1756
+ function deleteAgentFile(agentName: string, fileName: string): boolean {
1757
+ try {
1758
+ const filePath = path.join(getWorkspacePath(agentName), fileName);
1759
+ if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); return true; }
1760
+ return false;
1761
+ } catch { return false; }
1762
+ }
1763
+
1764
+ // ── Markdown Generators (aligned with markdownSerializer.ts patterns) ──
1765
+
1766
+ function generateIdentityMd(p: Record<string, any>): string {
1767
+ const lines: string[] = [];
1768
+ lines.push(`# ${p.agent_name}`);
1769
+ lines.push("");
1770
+ lines.push(`**Name:** ${p.agent_name}`);
1771
+ if (p.emoji) lines.push(`**Emoji:** ${p.emoji}`);
1772
+ lines.push(`**Role:** ${p.role || "Agent"}`);
1773
+ lines.push(`**Codename:** ${p.codename}`);
1774
+ if (p.voice_sample) lines.push(`**Sample:** ${p.voice_sample}`);
1775
+ lines.push("");
1776
+ if (p.one_liner) {
1777
+ lines.push("## One-Liner");
1778
+ lines.push("");
1779
+ lines.push(p.one_liner);
1780
+ lines.push("");
1781
+ }
1782
+ lines.push("## Core Identity");
1783
+ lines.push("");
1784
+ lines.push(p.core_identity || `${p.agent_name} is an AI agent specializing in ${p.role || "general tasks"}.`);
1785
+ lines.push("");
1786
+ lines.push("## Operating Style");
1787
+ lines.push("");
1788
+ lines.push(p.operating_style || "Focused, professional, detail-oriented.");
1789
+ lines.push("");
1790
+ lines.push("## Relationship to Team");
1791
+ lines.push("");
1792
+ lines.push(p.team_relationship || "Collaborative team member within the agent constellation.");
1793
+ lines.push("");
1794
+ return lines.join("\n");
1795
+ }
1796
+
1797
+ function generateSoulMd(p: Record<string, any>): string {
1798
+ const lines: string[] = [];
1799
+ lines.push(`# ${p.agent_name} — Soul`);
1800
+ lines.push("");
1801
+ lines.push("## Personality");
1802
+ lines.push("");
1803
+ lines.push(p.personality || `${p.agent_name} approaches work with focus and precision.`);
1804
+ lines.push("");
1805
+ lines.push("## Tone");
1806
+ lines.push("");
1807
+ lines.push(p.tone || "Professional, clear, and supportive.");
1808
+ lines.push("");
1809
+ lines.push("## Worldview");
1810
+ lines.push("");
1811
+ lines.push(p.worldview || "Every task has a structure; find the structure and the solution follows.");
1812
+ lines.push("");
1813
+ lines.push("## Emotional Logic");
1814
+ lines.push("");
1815
+ lines.push(p.emotional_logic || "Calm under pressure, energized by challenging problems.");
1816
+ lines.push("");
1817
+ lines.push("## Voice Markers");
1818
+ lines.push("");
1819
+ if (p.voice_markers && Array.isArray(p.voice_markers)) {
1820
+ for (const m of p.voice_markers) lines.push(`- ${m}`);
1821
+ } else {
1822
+ lines.push("- Direct and concise");
1823
+ lines.push("- Action-oriented language");
1824
+ }
1825
+ lines.push("");
1826
+ lines.push("## Pacing");
1827
+ lines.push("");
1828
+ lines.push(p.pacing || "Steady and methodical — never rushes, never stalls.");
1829
+ lines.push("");
1830
+ lines.push("## Forbidden Habits");
1831
+ lines.push("");
1832
+ if (p.forbidden_habits && Array.isArray(p.forbidden_habits)) {
1833
+ for (const h of p.forbidden_habits) lines.push(`- ${h}`);
1834
+ } else {
1835
+ lines.push("- Never make up data or references");
1836
+ lines.push("- Never ignore user constraints");
1837
+ }
1838
+ lines.push("");
1839
+ return lines.join("\n");
1840
+ }
1841
+
1842
+ function generateAgentsMd(p: Record<string, any>): string {
1843
+ const lines: string[] = [];
1844
+ lines.push(`# ${p.agent_name} — Agent Protocol`);
1845
+ lines.push("");
1846
+ lines.push(`**Title:** ${p.role || "Agent"}`);
1847
+ lines.push(`**Codename:** ${p.codename}`);
1848
+ lines.push("");
1849
+ lines.push("## Role Charter");
1850
+ lines.push("");
1851
+ lines.push("### Mission");
1852
+ lines.push("");
1853
+ lines.push(p.mission || `Serve as the ${p.role || "primary agent"} within the constellation.`);
1854
+ lines.push("");
1855
+ lines.push("### Scope of Responsibility");
1856
+ lines.push("");
1857
+ lines.push(p.scope || `All tasks related to ${p.role || "the assigned domain"}.`);
1858
+ lines.push("");
1859
+ lines.push("### Why This Role Exists");
1860
+ lines.push("");
1861
+ lines.push(p.why_role || `To provide dedicated ${p.role || "support"} capability within the team.`);
1862
+ lines.push("");
1863
+ lines.push("### Cost of Weakness");
1864
+ lines.push("");
1865
+ lines.push(p.cost_of_weakness || "Gaps in this role lead to bottlenecks and unresolved tasks.");
1866
+ lines.push("");
1867
+ lines.push("## Boundaries");
1868
+ lines.push("");
1869
+ lines.push("### Owns");
1870
+ lines.push("");
1871
+ if (p.owns && Array.isArray(p.owns)) {
1872
+ for (const o of p.owns) lines.push(`- ${o}`);
1873
+ } else {
1874
+ lines.push(`- ${p.role || "General"} domain tasks`);
1875
+ }
1876
+ lines.push("");
1877
+ lines.push("### Advises On");
1878
+ lines.push("");
1879
+ if (p.advises_on && Array.isArray(p.advises_on)) {
1880
+ for (const a of p.advises_on) lines.push(`- ${a}`);
1881
+ } else {
1882
+ lines.push("- Cross-functional decisions touching this domain");
1883
+ }
1884
+ lines.push("");
1885
+ lines.push("### Stays Out Of");
1886
+ lines.push("");
1887
+ if (p.stays_out_of && Array.isArray(p.stays_out_of)) {
1888
+ for (const s of p.stays_out_of) lines.push(`- ${s}`);
1889
+ } else {
1890
+ lines.push("- Other agents' primary domains");
1891
+ }
1892
+ lines.push("");
1893
+ lines.push("### Defers To");
1894
+ lines.push("");
1895
+ if (p.defers_to && Array.isArray(p.defers_to)) {
1896
+ for (const d of p.defers_to) lines.push(`- ${d}`);
1897
+ } else {
1898
+ lines.push("- The user for final decisions");
1899
+ }
1900
+ lines.push("");
1901
+ lines.push("## Operational Protocol");
1902
+ lines.push("");
1903
+ lines.push("### Default Behavior");
1904
+ lines.push("");
1905
+ lines.push(p.default_behavior || "Await instructions, proactively report status on active tasks.");
1906
+ lines.push("");
1907
+ lines.push("### Task Routing");
1908
+ lines.push("");
1909
+ lines.push("- Accept tasks within scope, flag tasks outside boundaries");
1910
+ lines.push("- Escalate blockers immediately");
1911
+ lines.push("");
1912
+ lines.push("### Response Discipline");
1913
+ lines.push("");
1914
+ lines.push("- Always confirm task understanding before execution");
1915
+ lines.push("- Provide structured output");
1916
+ lines.push("");
1917
+ return lines.join("\n");
1918
+ }
1919
+
1920
+ function generateToolsMd(p: Record<string, any>): string {
1921
+ const lines: string[] = [];
1922
+ lines.push(`# ${p.agent_name} — Tool Guide`);
1923
+ lines.push("");
1924
+ lines.push("## Available Tools");
1925
+ lines.push("");
1926
+ lines.push("- file_read / file_write — File system access");
1927
+ lines.push("- web_search — Internet research");
1928
+ lines.push("- shell — Terminal commands");
1929
+ lines.push("- browser — Web browsing");
1930
+ lines.push("- OFIERE meta-tools — Dashboard operations");
1931
+ lines.push("");
1932
+ lines.push("## Tool Usage Rules");
1933
+ lines.push("");
1934
+ lines.push("- Always use `file_read` to check existing content before overwriting");
1935
+ lines.push("- Prefer targeted edits over full file rewrites");
1936
+ lines.push("- Log all significant operations");
1937
+ lines.push("");
1938
+ lines.push("## Tool-Specific Notes");
1939
+ lines.push("");
1940
+ lines.push("### Memory");
1941
+ lines.push("");
1942
+ lines.push("Three-tier memory system:");
1943
+ lines.push("1. **Working Memory** — Current conversation context");
1944
+ lines.push("2. **Journal Layer** — Session summaries and decisions");
1945
+ lines.push("3. **Long-Term Core Facts** — Persistent user preferences and project context");
1946
+ lines.push("");
1947
+ return lines.join("\n");
1948
+ }
1949
+
1950
+ // ── Auto-register agent in OpenClaw (best-effort) ──
1951
+
1952
+ function tryRegisterAgent(agentName: string): { success: boolean; message: string } {
1953
+ const wsPath = getWorkspacePath(agentName);
1954
+ try {
1955
+ const cmd = `openclaw agents add "${agentName}" --workspace "${wsPath}" --non-interactive 2>&1`;
1956
+ const output = execSync(cmd, { encoding: "utf8", timeout: 15000 });
1957
+ api.logger?.info?.(`[ofiere] Auto-registered agent "${agentName}": ${output.slice(0, 200)}`);
1958
+ return { success: true, message: `Agent "${agentName}" registered in OpenClaw` };
1959
+ } catch (e: any) {
1960
+ const msg = e?.stderr || e?.stdout || String(e);
1961
+ // Check if already registered
1962
+ if (msg.includes("already exists") || msg.includes("duplicate")) {
1963
+ return { success: true, message: `Agent "${agentName}" was already registered` };
1964
+ }
1965
+ api.logger?.warn?.(`[ofiere] Auto-registration failed for "${agentName}": ${msg.slice(0, 300)}`);
1966
+ return { success: false, message: `Auto-registration failed. Manual step: openclaw agents add "${agentName}" --workspace "${wsPath}"` };
1967
+ }
1968
+ }
1969
+
1970
+ // ── Register the tool ──
1971
+
1972
+ api.registerTool({
1973
+ name: "OFIERE_CONSTELLATION_OPS",
1974
+ label: "Ofiere Constellation Operations",
1975
+ description:
1976
+ `Create, edit, and manage OpenClaw agent architectures directly from chat. ` +
1977
+ `This tool reads and writes the markdown files that define each agent's identity, personality, boundaries, and operational protocol.\n\n` +
1978
+ `Actions:\n` +
1979
+ `- "list_agents": List all agents with codename, role, and file listing\n` +
1980
+ `- "get_agent": Get full details for one agent. Required: agent_name\n` +
1981
+ `- "read_file": Read a specific agent file. Required: agent_name, file_name\n` +
1982
+ `- "write_file": Create or overwrite an agent file. Required: agent_name, file_name, content\n` +
1983
+ `- "create_agent": Scaffold a complete new agent. Required: agent_name, codename, role, emoji. ` +
1984
+ `Optional: mission, one_liner, personality, tone, worldview, emotional_logic, voice_sample, voice_markers (array), ` +
1985
+ `forbidden_habits (array), owns (array), advises_on (array), stays_out_of (array), defers_to (array), ` +
1986
+ `pacing, default_behavior, core_identity, operating_style, team_relationship, scope, why_role, cost_of_weakness, color_hex\n` +
1987
+ `- "delete_agent": PERMANENTLY delete an entire agent workspace and unregister from OpenClaw. Required: agent_name, confirm (must be true). ` +
1988
+ `⚠️ This is IRREVERSIBLE — always ask the user for explicit confirmation before calling this action.\n` +
1989
+ `- "delete_file": Remove a specific file. Required: agent_name, file_name\n` +
1990
+ `- "read_blueprint": Read the OpenClaw Agent Blueprint reference document\n` +
1991
+ `- "list_agent_mesh": Show the sovereignty map (who owns which domains)\n\n` +
1992
+ `IMPORTANT: Before creating or editing any agent, ALWAYS call read_blueprint first to understand the required structure.\n` +
1993
+ `IMPORTANT: Before calling delete_agent, ALWAYS ask the user for explicit confirmation. This action is IRREVERSIBLE.\n` +
1994
+ `New agents are created in workspace-<name>/ with IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, and a skills/ directory.`,
1995
+ parameters: {
1996
+ type: "object",
1997
+ required: ["action"],
1998
+ properties: {
1999
+ action: {
2000
+ type: "string",
2001
+ enum: ["list_agents", "get_agent", "read_file", "write_file", "create_agent", "delete_agent", "delete_file", "read_blueprint", "list_agent_mesh"],
2002
+ },
2003
+ agent_name: { type: "string", description: "Agent name (lowercase, e.g. 'luna', 'ivy')" },
2004
+ confirm: { type: "boolean", description: "Required for delete_agent. Must be true to execute. Always ask user for confirmation first." },
2005
+ file_name: { type: "string", description: "File name (e.g. 'SOUL.md', 'AGENTS.md')" },
2006
+ content: { type: "string", description: "File content for write_file" },
2007
+ // create_agent fields
2008
+ codename: { type: "string", description: "All-caps codename (e.g. 'PRISM', 'NEXUS')" },
2009
+ role: { type: "string", description: "Executive role (e.g. 'CTO', 'CMO', 'CPO')" },
2010
+ emoji: { type: "string", description: "Agent emoji (e.g. '🌙', '🎨')" },
2011
+ mission: { type: "string", description: "Agent's mission statement" },
2012
+ one_liner: { type: "string", description: "One-line agent description" },
2013
+ personality: { type: "string", description: "Personality description" },
2014
+ tone: { type: "string", description: "Communication tone" },
2015
+ worldview: { type: "string", description: "Core worldview/philosophy" },
2016
+ emotional_logic: { type: "string", description: "Emotional response patterns" },
2017
+ voice_sample: { type: "string", description: "Sample of how the agent talks" },
2018
+ voice_markers: { type: "array", items: { type: "string" }, description: "List of voice characteristics" },
2019
+ forbidden_habits: { type: "array", items: { type: "string" }, description: "Things the agent must never do" },
2020
+ owns: { type: "array", items: { type: "string" }, description: "Primary domain ownership" },
2021
+ advises_on: { type: "array", items: { type: "string" }, description: "Advisory areas" },
2022
+ stays_out_of: { type: "array", items: { type: "string" }, description: "No-fly zones" },
2023
+ defers_to: { type: "array", items: { type: "string" }, description: "Handoff targets" },
2024
+ pacing: { type: "string", description: "Work pacing description" },
2025
+ default_behavior: { type: "string", description: "Default operational behavior" },
2026
+ core_identity: { type: "string", description: "Core identity paragraph" },
2027
+ operating_style: { type: "string", description: "Operating style description" },
2028
+ team_relationship: { type: "string", description: "Relationship to the team" },
2029
+ scope: { type: "string", description: "Scope of responsibility" },
2030
+ why_role: { type: "string", description: "Why this role exists" },
2031
+ cost_of_weakness: { type: "string", description: "Cost of weakness in this role" },
2032
+ color_hex: { type: "string", description: "Brand color hex (e.g. '#a78bfa')" },
2033
+ },
2034
+ },
2035
+ async execute(_id: string, params: Record<string, unknown>) {
2036
+ const action = params.action as string;
2037
+
2038
+ switch (action) {
2039
+ // ── List all agents ──
2040
+ case "list_agents": {
2041
+ const agents = listWorkspaceAgents();
2042
+ return ok({
2043
+ agents: agents.map(a => ({
2044
+ name: a.name,
2045
+ codename: a.codename,
2046
+ role: a.role,
2047
+ file_count: a.files.length,
2048
+ files: a.files,
2049
+ has_skills: a.hasSkills,
2050
+ })),
2051
+ count: agents.length,
2052
+ });
2053
+ }
2054
+
2055
+ // ── Get agent details ──
2056
+ case "get_agent": {
2057
+ if (!params.agent_name) return err("Missing required: agent_name");
2058
+ const name = (params.agent_name as string).toLowerCase();
2059
+ const wsPath = getWorkspacePath(name);
2060
+ if (!fs.existsSync(wsPath)) return err(`Agent workspace not found: workspace-${name}`);
2061
+ const files = fs.readdirSync(wsPath).filter((f: string) => f.endsWith(".md"));
2062
+ const fileDetails = files.map((f: string) => {
2063
+ const stat = fs.statSync(path.join(wsPath, f));
2064
+ return { name: f, size_bytes: stat.size, modified: stat.mtime.toISOString() };
2065
+ });
2066
+ // Read IDENTITY.md for quick summary
2067
+ let identity: Record<string, string> = {};
2068
+ try {
2069
+ const idContent = fs.readFileSync(path.join(wsPath, "IDENTITY.md"), "utf8");
2070
+ const nameMatch = idContent.match(/\*\*Name[\s:]*\*\*[\s:]*(.+)/i);
2071
+ const cnMatch = idContent.match(/\*\*Codename[\s:]*\*\*[\s:]*(.+)/i);
2072
+ const roleMatch = idContent.match(/\*\*Role[\s:]*\*\*[\s:]*(.+)/i);
2073
+ const emojiMatch = idContent.match(/\*\*Emoji[\s:]*\*\*[\s:]*(.+)/i);
2074
+ if (nameMatch) identity.name = nameMatch[1].trim();
2075
+ if (cnMatch) identity.codename = cnMatch[1].trim();
2076
+ if (roleMatch) identity.role = roleMatch[1].trim();
2077
+ if (emojiMatch) identity.emoji = emojiMatch[1].trim();
2078
+ } catch {}
2079
+ const hasSkills = fs.existsSync(path.join(wsPath, "skills"));
2080
+ return ok({ agent_name: name, identity, files: fileDetails, has_skills: hasSkills, workspace_path: wsPath });
2081
+ }
2082
+
2083
+ // ── Read a file ──
2084
+ case "read_file": {
2085
+ if (!params.agent_name || !params.file_name) return err("Missing required: agent_name, file_name");
2086
+ const content = readAgentFile((params.agent_name as string).toLowerCase(), params.file_name as string);
2087
+ if (content === null) return err(`File not found: ${params.file_name} in workspace-${params.agent_name}`);
2088
+ return ok({ agent_name: params.agent_name, file_name: params.file_name, content, size_bytes: Buffer.byteLength(content) });
2089
+ }
2090
+
2091
+ // ── Write a file ──
2092
+ case "write_file": {
2093
+ if (!params.agent_name || !params.file_name || !params.content)
2094
+ return err("Missing required: agent_name, file_name, content");
2095
+ const agentName = (params.agent_name as string).toLowerCase();
2096
+ writeAgentFile(agentName, params.file_name as string, params.content as string);
2097
+ api.logger?.info?.(`[ofiere] Wrote ${params.file_name} for agent "${agentName}"`);
2098
+ return ok({
2099
+ message: `File "${params.file_name}" written to workspace-${agentName}`,
2100
+ agent_name: agentName,
2101
+ file_name: params.file_name,
2102
+ size_bytes: Buffer.byteLength(params.content as string),
2103
+ });
2104
+ }
2105
+
2106
+ // ── Create a full agent ──
2107
+ case "create_agent": {
2108
+ if (!params.agent_name || !params.codename || !params.role || !params.emoji)
2109
+ return err("Missing required: agent_name, codename, role, emoji");
2110
+
2111
+ const agentName = (params.agent_name as string).toLowerCase().replace(/[^a-z0-9_-]/g, "");
2112
+ const wsPath = getWorkspacePath(agentName);
2113
+
2114
+ // Check if workspace already exists
2115
+ if (fs.existsSync(wsPath)) {
2116
+ const existing = fs.readdirSync(wsPath).filter((f: string) => f.endsWith(".md"));
2117
+ if (existing.length > 0) {
2118
+ return err(`Agent workspace-${agentName} already exists with ${existing.length} files. Use write_file to edit individual files instead.`);
2119
+ }
2120
+ }
2121
+
2122
+ // Create workspace directory
2123
+ fs.mkdirSync(wsPath, { recursive: true, mode: 0o777 });
2124
+
2125
+ // Create skills directory
2126
+ const skillsDir = path.join(wsPath, "skills");
2127
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true, mode: 0o777 });
2128
+
2129
+ // Create .openclaw subdirectory (workspace state)
2130
+ const ocDir = path.join(wsPath, ".openclaw");
2131
+ if (!fs.existsSync(ocDir)) fs.mkdirSync(ocDir, { recursive: true, mode: 0o777 });
2132
+
2133
+ // Generate all files
2134
+ const p = params as Record<string, any>;
2135
+ const identityMd = generateIdentityMd(p);
2136
+ const soulMd = generateSoulMd(p);
2137
+ const agentsMd = generateAgentsMd(p);
2138
+ const toolsMd = generateToolsMd(p);
2139
+
2140
+ // Write files
2141
+ const filesWritten: string[] = [];
2142
+ writeAgentFile(agentName, "IDENTITY.md", identityMd); filesWritten.push("IDENTITY.md");
2143
+ writeAgentFile(agentName, "SOUL.md", soulMd); filesWritten.push("SOUL.md");
2144
+ writeAgentFile(agentName, "AGENTS.md", agentsMd); filesWritten.push("AGENTS.md");
2145
+ writeAgentFile(agentName, "TOOLS.md", toolsMd); filesWritten.push("TOOLS.md");
2146
+
2147
+ // Create doctrine file (codename.md) — empty scaffold
2148
+ const doctrineName = `${(params.codename as string).toUpperCase()}.md`;
2149
+ const doctrineMd = `# ${params.codename} Doctrine\n\n## Mission\n\n${p.mission || "To be defined."}\n\n## Decision Frameworks\n\n- (Define frameworks here)\n\n## Non-Goals\n\n- (Define non-goals here)\n\n## Evaluation Criteria\n\n- (Define criteria here)\n\n## Metrics\n\n- (Define metrics here)\n\n## Standard Deliverables\n\n- (Define deliverables here)\n\n## Anti-Patterns\n\n- (Define anti-patterns here)\n\n## Handoff Rules\n\n- (Define handoff rules here)\n`;
2150
+ writeAgentFile(agentName, doctrineName, doctrineMd);
2151
+ filesWritten.push(doctrineName);
2152
+
2153
+ api.logger?.info?.(`[ofiere] Created agent "${agentName}" with ${filesWritten.length} files`);
2154
+
2155
+ // Try auto-registration
2156
+ const regResult = tryRegisterAgent(agentName);
2157
+
2158
+ // Compute a basic build score
2159
+ const buildAssessment = {
2160
+ identity: true,
2161
+ soul: true,
2162
+ agents: true,
2163
+ tools: true,
2164
+ doctrine: true,
2165
+ skills_dir: true,
2166
+ total_files: filesWritten.length,
2167
+ completeness: "Scaffold complete — flesh out personality, doctrine, and playbooks for a full build",
2168
+ };
2169
+
2170
+ return ok({
2171
+ message: `Agent "${agentName}" (${params.codename}) created with ${filesWritten.length} files`,
2172
+ agent_name: agentName,
2173
+ codename: params.codename,
2174
+ role: params.role,
2175
+ emoji: params.emoji,
2176
+ workspace_path: wsPath,
2177
+ files_created: filesWritten,
2178
+ registration: regResult,
2179
+ build_assessment: buildAssessment,
2180
+ next_steps: [
2181
+ "Flesh out SOUL.md with richer personality and voice markers",
2182
+ `Expand ${doctrineName} with decision frameworks and metrics`,
2183
+ "Add skills to the skills/ directory for specialized capabilities",
2184
+ "Add SOUL_EXTENDED.md for deeper character development",
2185
+ "Use the Constellation page in the dashboard to visually review the build score",
2186
+ ],
2187
+ });
2188
+ }
2189
+
2190
+ // ── Delete a file ──
2191
+ case "delete_file": {
2192
+ if (!params.agent_name || !params.file_name) return err("Missing required: agent_name, file_name");
2193
+ const agentName = (params.agent_name as string).toLowerCase();
2194
+ const deleted = deleteAgentFile(agentName, params.file_name as string);
2195
+ if (!deleted) return err(`File not found: ${params.file_name} in workspace-${agentName}`);
2196
+ api.logger?.info?.(`[ofiere] Deleted ${params.file_name} from agent "${agentName}"`);
2197
+ return ok({ message: `File "${params.file_name}" deleted from workspace-${agentName}`, ok: true });
2198
+ }
2199
+
2200
+ // ── Read the blueprint ──
2201
+ case "read_blueprint": {
2202
+ // Try multiple locations for the blueprint
2203
+ const candidates = [
2204
+ path.join(OPENCLAW_ROOT, "AGENT_BLUEPRINT.md"),
2205
+ path.join(OPENCLAW_ROOT, "OPENCLAW_AGENT_BLUEPRINT.md"),
2206
+ ];
2207
+ for (const bp of candidates) {
2208
+ try {
2209
+ const content = fs.readFileSync(bp, "utf8");
2210
+ return ok({ blueprint: content, source: bp, size_bytes: Buffer.byteLength(content) });
2211
+ } catch {}
2212
+ }
2213
+ return err(
2214
+ "Agent blueprint not found. Expected at: " + candidates.join(" or ") +
2215
+ ". The blueprint defines the mandatory structure for all OpenClaw agents."
2216
+ );
2217
+ }
2218
+
2219
+ // ── Agent Mesh — sovereignty map ──
2220
+ case "list_agent_mesh": {
2221
+ const agents = listWorkspaceAgents();
2222
+ const mesh: Array<{ agent: string; codename: string; role: string; owns: string[] }> = [];
2223
+ for (const agent of agents) {
2224
+ const agentsContent = readAgentFile(agent.name, "AGENTS.md");
2225
+ const owns: string[] = [];
2226
+ if (agentsContent) {
2227
+ // Extract ### Owns section
2228
+ const ownsMatch = agentsContent.match(/###\s+Owns\s*\n([\s\S]*?)(?=\n###|\n##|$)/i);
2229
+ if (ownsMatch) {
2230
+ const items = ownsMatch[1].split("\n")
2231
+ .map((l: string) => l.replace(/^[\s-*]+/, "").trim())
2232
+ .filter((l: string) => l.length > 0);
2233
+ owns.push(...items);
2234
+ }
2235
+ }
2236
+ mesh.push({ agent: agent.name, codename: agent.codename, role: agent.role, owns });
2237
+ }
2238
+ return ok({ mesh, count: mesh.length });
2239
+ }
2240
+
2241
+ // ── Delete an entire agent ──
2242
+ case "delete_agent": {
2243
+ if (!params.agent_name) return err("Missing required: agent_name");
2244
+ const agentName = (params.agent_name as string).toLowerCase().replace(/[^a-z0-9_-]/g, "");
2245
+ const wsPath = getWorkspacePath(agentName);
2246
+
2247
+ // Safety gate: confirm must be explicitly true
2248
+ if (params.confirm !== true) {
2249
+ // List what would be deleted so the agent can show the user
2250
+ let fileList: string[] = [];
2251
+ try {
2252
+ if (fs.existsSync(wsPath)) {
2253
+ fileList = fs.readdirSync(wsPath, { recursive: true }) as string[];
2254
+ }
2255
+ } catch {}
2256
+ return err(
2257
+ `⚠️ DESTRUCTIVE ACTION: This will permanently delete agent "${agentName}" and ALL files in workspace-${agentName}/ (${fileList.length} items). ` +
2258
+ `This CANNOT be undone.\n\n` +
2259
+ `Files that will be deleted:\n${fileList.map((f: string) => ` - ${f}`).join("\n") || " (directory not found)"}\n\n` +
2260
+ `To proceed, ask the user for explicit confirmation and then call this action again with confirm: true.`
2261
+ );
2262
+ }
2263
+
2264
+ // Check workspace exists
2265
+ if (!fs.existsSync(wsPath)) {
2266
+ return err(`Agent workspace-${agentName} does not exist at ${wsPath}`);
2267
+ }
2268
+
2269
+ // Count files before deletion for the report
2270
+ let deletedFiles: string[] = [];
2271
+ try {
2272
+ deletedFiles = fs.readdirSync(wsPath, { recursive: true }) as string[];
2273
+ } catch {}
2274
+
2275
+ // Remove the entire workspace directory
2276
+ try {
2277
+ fs.rmSync(wsPath, { recursive: true, force: true });
2278
+ } catch (e: any) {
2279
+ return err(`Failed to delete workspace-${agentName}: ${String(e).slice(0, 300)}`);
2280
+ }
2281
+
2282
+ // Unregister from OpenClaw (best-effort)
2283
+ let unregResult = { success: false, message: "" };
2284
+ try {
2285
+ const cmd = `openclaw agents remove "${agentName}" --non-interactive 2>&1`;
2286
+ const output = execSync(cmd, { encoding: "utf8", timeout: 15000 });
2287
+ unregResult = { success: true, message: output.slice(0, 200) };
2288
+ } catch (e: any) {
2289
+ unregResult = { success: false, message: `Manual step needed: openclaw agents remove "${agentName}"` };
2290
+ }
2291
+
2292
+ api.logger?.info?.(`[ofiere] Deleted agent "${agentName}" — ${deletedFiles.length} files removed`);
2293
+
2294
+ return ok({
2295
+ message: `Agent "${agentName}" has been permanently deleted`,
2296
+ agent_name: agentName,
2297
+ workspace_deleted: wsPath,
2298
+ files_removed: deletedFiles.length,
2299
+ unregistration: unregResult,
2300
+ });
2301
+ }
2302
+
2303
+ default:
2304
+ return err(`Unknown action "${action}".`);
2305
+ }
2306
+ },
2307
+ });
2308
+ }
2309
+
1676
2310
  // ═══════════════════════════════════════════════════════════════════════════════
1677
2311
  // Public: Register All Meta-Tools
1678
2312
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1699,9 +2333,10 @@ export function registerTools(
1699
2333
  registerNotifyOps(api, supabase, userId); // 7
1700
2334
  registerMemoryOps(api, supabase, userId); // 8
1701
2335
  registerPromptOps(api, supabase, userId); // 9
2336
+ registerConstellationOps(api, supabase, userId); // 10
1702
2337
 
1703
2338
  // ── Count and log ──
1704
- const toolCount = 9;
2339
+ const toolCount = 10;
1705
2340
  const callerName = getCallingAgentName(api);
1706
2341
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
1707
2342
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);