diff --git a/README.md b/README.md index 720bb5e..6500eae 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,6 @@ Full parameters and usage in the [Tools Reference](docs/TOOLS.md). | **[Configuration](docs/CONFIGURATION.md)** | `openclaw.json`, `projects.json`, heartbeat, notifications | | **[Onboarding Guide](docs/ONBOARDING.md)** | Full step-by-step setup | | **[QA Workflow](docs/QA_WORKFLOW.md)** | QA process and review templates | -| **[Context Awareness](docs/CONTEXT-AWARENESS.md)** | How tools adapt to group vs. DM vs. agent context | | **[Testing](docs/TESTING.md)** | Test suite, fixtures, CI/CD | | **[Management Theory](docs/MANAGEMENT.md)** | The delegation model behind the design | | **[Roadmap](docs/ROADMAP.md)** | What's coming next | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7bc4a98..e4323b7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -135,24 +135,6 @@ Control which lifecycle events send notifications: | `workerStart` | `true` | Announce when a worker picks up a task | | `workerComplete` | `true` | Announce when a worker finishes a task | -### DevClaw Agent IDs - -List which agents are recognized as DevClaw orchestrators (used for context detection): - -```json -{ - "plugins": { - "entries": { - "devclaw": { - "config": { - "devClawAgentIds": ["my-orchestrator"] - } - } - } - } -} -``` - ### Agent Tool Permissions Restrict DevClaw tools to your orchestrator agent: diff --git a/docs/CONTEXT-AWARENESS.md b/docs/CONTEXT-AWARENESS.md deleted file mode 100644 index 2070e64..0000000 --- a/docs/CONTEXT-AWARENESS.md +++ /dev/null @@ -1,133 +0,0 @@ -# DevClaw — Context Awareness - -DevClaw adapts its behavior based on how you interact with it. - -## Design Philosophy - -**One Group = One Project = One Team** - -DevClaw enforces strict boundaries between projects: -- Each Telegram/WhatsApp group represents a **single project** -- Each project has its **own dedicated dev/qa workers** -- Project work happens **inside that project's group** -- Setup and configuration happen **outside project groups** - -This prevents: -- Cross-project contamination (workers picking up wrong project's tasks) -- Confusion about which project you're working on -- Accidental registration of wrong groups -- Setup discussions cluttering project work channels - -This enables: -- Clear mental model: "This group = this project" -- Isolated work streams: Each project progresses independently -- Dedicated teams: Workers focus on one project at a time -- Clean separation: Setup vs. operational work - -## Three Interaction Contexts - -### 1. Via Another Agent (Setup Mode) - -When you talk to your main agent about DevClaw: -- Use: `onboard`, `setup` -- Avoid: `work_start`, `status` (operational tools) - -**Example:** -``` -User → Main Agent: "Can you help me set up DevClaw?" -Main Agent → Calls onboard -``` - -### 2. Direct Message to DevClaw Agent - -When you DM the DevClaw agent directly on Telegram/WhatsApp: -- Use: `status` (all projects), `health` (system overview) -- Avoid: `work_start` (project-specific work), setup tools - -**Example:** -``` -User → DevClaw DM: "Show me the status of all projects" -DevClaw → Calls status (shows all projects) -``` - -### 3. Project Group Chat - -When you message in a Telegram/WhatsApp group bound to a project: -- Use: `work_start`, `work_finish`, `task_create`, `status` (auto-filtered) -- Avoid: Setup tools, system-wide queries - -**Example:** -``` -User → Project Group: "pick up issue #42" -DevClaw → Calls work_start (only works in groups) -``` - -## How It Works - -### Context Detection - -Each tool automatically detects: -- **Agent ID** — Is this the DevClaw agent or another agent? -- **Message Channel** — Telegram, WhatsApp, or CLI? -- **Session Key** — Is this a group chat or direct message? - - Format: `agent:{agentId}:{channel}:{type}:{id}` - - Telegram group: `agent:devclaw:telegram:group:-5266044536` - - WhatsApp group: `agent:devclaw:whatsapp:group:120363123@g.us` - - DM: `agent:devclaw:telegram:user:657120585` -- **Project Binding** — Which project is this group bound to? - -### Guardrails - -Tools include context-aware guidance in their responses: -```json -{ - "contextGuidance": "Context: Project Group Chat (telegram)\n You're in a Telegram group for project 'my-webapp'.\n Use work_start, work_finish for project work.", - ... -} -``` - -## Tool Context Requirements - -| Tool | Group chat | Direct DM | Via agent | -|---|---|---|---| -| `onboard` | Blocked | Works | Works | -| `setup` | Works | Works | Works | -| `work_start` | Works | Blocked | Blocked | -| `work_finish` | Works | Works | Works | -| `task_create` | Works | Works | Works | -| `task_update` | Works | Works | Works | -| `task_comment` | Works | Works | Works | -| `status` | Auto-filtered | All projects | Suggests onboard | -| `health` | Auto-filtered | All projects | Works | -| `work_heartbeat` | Single project | All projects | Works | -| `project_register` | Works (required) | Blocked | Blocked | - -**Why `project_register` requires group context:** -- Forces deliberate project registration from within the project's space -- You're physically in the group when binding it, making the connection explicit -- Impossible to accidentally register the wrong group - -## WhatsApp Support - -DevClaw fully supports WhatsApp groups with the same architecture as Telegram: - -- WhatsApp group detection via `sessionKey.includes("@g.us")` -- Projects keyed by WhatsApp group ID (e.g., `"120363123@g.us"`) -- Context-aware tools work identically for both channels -- One project = one group (Telegram OR WhatsApp) - -**To register a WhatsApp project:** -1. Go to the WhatsApp group chat -2. Call `project_register` from within the group -3. Group ID auto-detected from context - -## Implementation - -- **Module:** [`lib/context-guard.ts`](../lib/context-guard.ts) -- **Detection logic:** Checks agentId, messageChannel, sessionKey pattern matching -- **Configuration:** `devClawAgentIds` in plugin config lists which agents are DevClaw orchestrators - -## Related - -- [Configuration — devClawAgentIds](CONFIGURATION.md#devclaw-agent-ids) -- [Architecture — Scope boundaries](ARCHITECTURE.md#scope-boundaries) diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 8c586ba..85915c4 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -320,7 +320,7 @@ Conversational onboarding guide. Returns step-by-step instructions for the agent **Source:** [`lib/tools/onboard.ts`](../lib/tools/onboard.ts) -**Context:** Works in DMs and via-agent. Blocks group chats (setup should not happen in project groups). +**Note:** Call this before `setup` to get step-by-step guidance. **Parameters:** diff --git a/index.ts b/index.ts index 1a8be44..dcafa9c 100644 --- a/index.ts +++ b/index.ts @@ -96,9 +96,9 @@ const plugin = { // Operations api.registerTool(createStatusTool(api), { names: ["status"] }); - api.registerTool(createHealthTool(api), { names: ["health"] }); + api.registerTool(createHealthTool(), { names: ["health"] }); // Setup & config - api.registerTool(createProjectRegisterTool(api), { + api.registerTool(createProjectRegisterTool(), { names: ["project_register"], }); api.registerTool(createSetupTool(api), { names: ["setup"] }); diff --git a/lib/context-guard.ts b/lib/context-guard.ts deleted file mode 100644 index e2671f5..0000000 --- a/lib/context-guard.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * context-guard.ts — Detect interaction context and provide guardrails. - * - * DevClaw should respond differently based on how it's being contacted: - * 1. Via another agent (setup/onboarding) - guide to onboard/setup - * 2. Direct to DevClaw agent (status queries) - use status - * 3. Via Telegram group (project work) - use work_start, work_finish, task_create - */ -import type { ToolContext } from "./types.js"; -import fs from "node:fs/promises"; -import path from "node:path"; - -export type InteractionContext = - | { type: "via-agent"; agentId: string; agentName?: string } - | { type: "direct"; channel?: string; chatId?: string } - | { - type: "group"; - channel: string; - groupId: string; - projectName?: string; - }; - -/** - * Detect the interaction context from ToolContext. - * - * Logic: - * - If agentId doesn't match a known DevClaw agent → via-agent - * - If messageChannel + sessionKey contains group ID → group - * - Otherwise → direct - */ -export async function detectContext( - ctx: ToolContext, - devClawAgentIds: string[], -): Promise { - const { agentId, messageChannel, sessionKey } = ctx; - - // --- Via another agent (not DevClaw) --- - if (agentId && !devClawAgentIds.includes(agentId)) { - return { - type: "via-agent", - agentId, - // agentName could be resolved from openclaw.json if needed - }; - } - - // --- Group chat (has messageChannel + group-like sessionKey) --- - if (messageChannel && sessionKey) { - // sessionKey format: "agent:{agentId}:{channel}:{type}:{groupId}" - // Examples: - // - Telegram: "agent:devclaw:telegram:group:-5266044536" - // - WhatsApp: "agent:devclaw:whatsapp:group:120363123@g.us" - const isGroupLike = sessionKey.includes(":group:"); - - if (isGroupLike) { - // Extract the actual group ID (last component after splitting) - const parts = sessionKey.split(":"); - const actualGroupId = parts[parts.length - 1]; - - // Try to match with a registered project - const projectName = await findProjectByGroupId( - actualGroupId, - ctx.workspaceDir, - ); - - return { - type: "group", - channel: messageChannel, - groupId: actualGroupId, - projectName, - }; - } - } - - // --- Direct (DM or CLI) --- - // Extract chat ID from sessionKey (e.g. "agent:devclaw:telegram:direct:657120585") - const chatId = sessionKey ? sessionKey.split(":").pop() : undefined; - return { - type: "direct", - channel: messageChannel ?? "cli", - chatId, - }; -} - -/** - * Generate guardrail guidance based on context. - * - * Returns a message to prepend to tool results or inject into system context. - */ -export function generateGuardrails(context: InteractionContext): string { - switch (context.type) { - case "via-agent": - return `## 🛡️ Context: Setup Mode (via ${context.agentId}) - -You're being called by another agent. This is likely a **setup or onboarding** scenario. - -**What you should do:** -- If the user mentions "setup", "install", "configure", or "onboard" → call \`onboard\` first -- Then follow the guidance to call \`setup\` with collected answers -- After setup, offer to register a project via \`project_register\` - -**What to avoid:** -- Don't discuss ongoing development tasks (those happen in group chats) -- Don't use work_start/work_finish/status (not relevant during setup) -`; - - case "direct": - return `## 🛡️ Context: Direct Communication (${context.channel}) - -You're in a **direct message** with the DevClaw agent (not a project group). - -**What you should do:** -- Provide **general status** via \`status\` (across all projects) -- Answer questions about DevClaw configuration -- Guide to project-specific work: "For project tasks, please message the relevant Telegram/WhatsApp group" - -**What to avoid:** -- Don't start development tasks here (use \`work_start\` only in project groups) -- Don't discuss project-specific issues (redirect to the group) -`; - - case "group": - return `## 🛡️ Context: Project Group Chat (${context.channel}) - -You're in a **Telegram/WhatsApp group** bound to ${context.projectName ? `project **${context.projectName}**` : "a project"}. - -**What you should do:** -- Handle task lifecycle: \`work_start\` (start work), \`work_finish\` (finish) -- Create new issues via \`task_create\` -- Check this project's queue via \`status\` -- Discuss implementation details, code reviews, bugs - -**What to avoid:** -- Don't discuss DevClaw setup (that's for direct DMs or via another agent) -- Don't show status for unrelated projects (focus on this group's project) -`; - - default: - return ""; - } -} - -/** - * Find project name by matching groupId in projects/projects.json. - * The groupId (Telegram or WhatsApp) is the KEY in the projects Record. - */ -async function findProjectByGroupId( - groupId: string, - workspaceDir?: string, -): Promise { - if (!workspaceDir) return undefined; - - try { - const projectsPath = path.join(workspaceDir, "projects", "projects.json"); - const raw = await fs.readFile(projectsPath, "utf-8"); - const data = JSON.parse(raw) as { - projects: Record; - }; - - // groupId IS the key in the Record - return data.projects[groupId]?.name; - } catch { - // File doesn't exist or parse error - } - - return undefined; -} diff --git a/lib/services/heartbeat.ts b/lib/services/heartbeat.ts index a80be84..bb2e2f8 100644 --- a/lib/services/heartbeat.ts +++ b/lib/services/heartbeat.ts @@ -11,6 +11,8 @@ * Workers only consume tokens when they start processing dispatched tasks. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import fs from "node:fs"; +import path from "node:path"; import { readProjects } from "../projects.js"; import { log as auditLog } from "../audit.js"; import { checkWorkerHealth } from "./health.js"; @@ -42,9 +44,6 @@ type ServiceContext = { logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void }; config: { agents?: { list?: Array<{ id: string; workspace?: string }> }; - plugins?: { - entries?: { devclaw?: { config?: { devClawAgentIds?: string[] } } }; - }; }; }; @@ -115,21 +114,22 @@ export function registerHeartbeatService(api: OpenClawPluginApi) { // --------------------------------------------------------------------------- /** - * Extract DevClaw agents from OpenClaw configuration. + * Discover DevClaw agents by scanning which agent workspaces have projects. + * Self-discovering: any agent whose workspace contains projects/projects.json is processed. */ function discoverAgents(config: ServiceContext["config"]): Agent[] { - const devClawAgentIds = config.plugins?.entries?.devclaw?.config?.devClawAgentIds || []; const agentsList = config.agents?.list || []; - return devClawAgentIds - .map((agentId) => { - const agent = agentsList.find((a) => a.id === agentId); - return { - agentId, - workspace: agent?.workspace || "", - }; + return agentsList + .filter((a): a is { id: string; workspace: string } => { + if (!a.workspace) return false; + try { + return fs.existsSync(path.join(a.workspace, "projects", "projects.json")); + } catch { + return false; + } }) - .filter((a): a is Agent => !!a.workspace); + .map((a) => ({ agentId: a.id, workspace: a.workspace })); } /** diff --git a/lib/setup/config.ts b/lib/setup/config.ts index 2d4b89d..2384b6b 100644 --- a/lib/setup/config.ts +++ b/lib/setup/config.ts @@ -1,7 +1,7 @@ /** * setup/config.ts — Plugin config writer (openclaw.json). * - * Handles: model level config, devClawAgentIds, tool restrictions, subagent cleanup. + * Handles: model level config, tool restrictions, subagent cleanup. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js"; @@ -9,7 +9,7 @@ import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js"; type ModelConfig = { dev: Record; qa: Record }; /** - * Write DevClaw model level config and devClawAgentIds to openclaw.json plugins section. + * Write DevClaw model level config to openclaw.json plugins section. * * Also configures: * - Tool restrictions (deny sessions_spawn, sessions_send) for DevClaw agents @@ -36,7 +36,6 @@ export async function writePluginConfig( configureSubagentCleanup(config); if (agentId) { - addDevClawAgentId(config, agentId); addToolRestrictions(config, agentId); } @@ -66,14 +65,6 @@ function configureSubagentCleanup(config: Record): void { (defaults.subagents as Record).archiveAfterMinutes = 43200; } -function addDevClawAgentId(config: Record, agentId: string): void { - const devclaw = (config as any).plugins.entries.devclaw.config; - const existing: string[] = devclaw.devClawAgentIds ?? []; - if (!existing.includes(agentId)) { - devclaw.devClawAgentIds = [...existing, agentId]; - } -} - function addToolRestrictions(config: Record, agentId: string): void { const agent = (config as any).agents?.list?.find((a: { id: string }) => a.id === agentId); if (agent) { diff --git a/lib/tool-helpers.ts b/lib/tool-helpers.ts index 3f54aaf..1d436b7 100644 --- a/lib/tool-helpers.ts +++ b/lib/tool-helpers.ts @@ -2,13 +2,10 @@ * tool-helpers.ts — Shared resolution helpers for tool execute() functions. * * Eliminates repeated boilerplate across tools: workspace validation, - * context detection, project resolution, provider creation. + * project resolution, provider creation. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; -import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "./types.js"; -import type { InteractionContext } from "./context-guard.js"; -import { detectContext, generateGuardrails } from "./context-guard.js"; import { readProjects, getProject, type Project, type ProjectsData } from "./projects.js"; import { createProvider, type ProviderWithType } from "./providers/index.js"; import { projectTick, type TickAction } from "./services/tick.js"; @@ -24,19 +21,6 @@ export function requireWorkspaceDir(ctx: ToolContext): string { return ctx.workspaceDir; } -/** - * Detect interaction context (via-agent, direct, or group). - * Extracts devClawAgentIds from plugin config automatically. - */ -export async function resolveContext( - ctx: ToolContext, - api: OpenClawPluginApi, -): Promise { - const devClawAgentIds = - ((api.pluginConfig as Record)?.devClawAgentIds as string[] | undefined) ?? []; - return detectContext(ctx, devClawAgentIds); -} - /** * Resolve project by groupId, throw if not found. */ @@ -59,20 +43,6 @@ export function resolveProvider(project: Project): ProviderWithType { return createProvider({ repo: project.repo }); } -/** - * Return a standard "group-only" error response for tools restricted to group chats. - */ -export function groupOnlyError(toolName: string, context: InteractionContext) { - return jsonResult({ - success: false, - error: `${toolName} can only be used in project group chats.`, - recommendation: context.type === "via-agent" - ? "Use onboard instead for setup." - : "Use the relevant project group.", - contextGuidance: generateGuardrails(context), - }); -} - /** * Get plugin config as a typed record (or undefined). */ diff --git a/lib/tools/health.ts b/lib/tools/health.ts index ad24c68..1b2bc4b 100644 --- a/lib/tools/health.ts +++ b/lib/tools/health.ts @@ -2,17 +2,15 @@ * health — Worker health scan with optional auto-fix. * * Read-only by default (surfaces issues). Pass fix=true to apply fixes. - * Context-aware: auto-filters to project in group chats. */ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; -import { readProjects, getProject, type Project } from "../projects.js"; +import { readProjects, getProject } from "../projects.js"; import { log as auditLog } from "../audit.js"; import { checkWorkerHealth, type HealthFix } from "../services/health.js"; -import { requireWorkspaceDir, resolveContext, resolveProvider } from "../tool-helpers.js"; +import { requireWorkspaceDir, resolveProvider } from "../tool-helpers.js"; -export function createHealthTool(api: OpenClawPluginApi) { +export function createHealthTool() { return (ctx: ToolContext) => ({ name: "health", label: "Health", @@ -31,10 +29,7 @@ export function createHealthTool(api: OpenClawPluginApi) { const fix = (params.fix as boolean) ?? false; const activeSessions = (params.activeSessions as string[]) ?? []; - // Auto-filter in group context - const context = await resolveContext(ctx, api); - let groupId = params.projectGroupId as string | undefined; - if (context.type === "group" && !groupId) groupId = context.groupId; + const groupId = params.projectGroupId as string | undefined; const data = await readProjects(workspaceDir); const projectIds = groupId ? [groupId] : Object.keys(data.projects); diff --git a/lib/tools/onboard.ts b/lib/tools/onboard.ts index 2c08a74..d6296f6 100644 --- a/lib/tools/onboard.ts +++ b/lib/tools/onboard.ts @@ -7,7 +7,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; import { isPluginConfigured, hasWorkspaceFiles, buildOnboardToolContext, buildReconfigContext } from "../onboarding.js"; -import { detectContext, generateGuardrails } from "../context-guard.js"; export function createOnboardTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ @@ -22,17 +21,6 @@ export function createOnboardTool(api: OpenClawPluginApi) { }, async execute(_id: string, params: Record) { - const devClawAgentIds = ((api.pluginConfig as Record)?.devClawAgentIds as string[] | undefined) ?? []; - const context = await detectContext(ctx, devClawAgentIds); - - if (context.type === "group") { - return jsonResult({ - success: false, error: "Onboarding should not be done in group chats.", - recommendation: "Use a direct message instead.", - contextGuidance: generateGuardrails(context), - }); - } - const configured = isPluginConfigured(api.pluginConfig as Record); const hasWorkspace = await hasWorkspaceFiles(ctx.workspaceDir); const mode = params.mode ? (params.mode as "first-run" | "reconfigure") @@ -42,7 +30,6 @@ export function createOnboardTool(api: OpenClawPluginApi) { return jsonResult({ success: true, mode, configured, instructions, - contextGuidance: generateGuardrails(context), nextSteps: ["Follow instructions above", "Call setup with collected answers", mode === "first-run" ? "Register a project afterward" : null].filter(Boolean), }); }, diff --git a/lib/tools/project-register.ts b/lib/tools/project-register.ts index fef9862..172e2b4 100644 --- a/lib/tools/project-register.ts +++ b/lib/tools/project-register.ts @@ -6,7 +6,6 @@ * * Replaces the manual steps of running glab/gh label create + editing projects.json. */ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; import fs from "node:fs/promises"; @@ -17,7 +16,6 @@ import { createProvider } from "../providers/index.js"; import { log as auditLog } from "../audit.js"; import { DEV_LEVELS, QA_LEVELS } from "../tiers.js"; import { DEFAULT_DEV_INSTRUCTIONS, DEFAULT_QA_INSTRUCTIONS } from "../templates.js"; -import { detectContext, generateGuardrails } from "../context-guard.js"; /** * Scaffold project-specific prompt files. @@ -48,18 +46,18 @@ async function scaffoldPromptFiles(workspaceDir: string, projectName: string): P return created; } -export function createProjectRegisterTool(api: OpenClawPluginApi) { +export function createProjectRegisterTool() { return (ctx: ToolContext) => ({ name: "project_register", label: "Project Register", - description: `Register a new project with DevClaw. ONLY works in the Telegram/WhatsApp group you're registering. Creates state labels, adds to projects.json, auto-populates group ID. One-time setup per project.`, + description: `Register a new project with DevClaw. Creates state labels, adds to projects.json. One-time setup per project.`, parameters: { type: "object", - required: ["name", "repo", "baseBranch"], + required: ["projectGroupId", "name", "repo", "baseBranch"], properties: { projectGroupId: { type: "string", - description: "Telegram/WhatsApp group ID (optional - auto-detected from current group if omitted)", + description: "Project group ID (e.g. Telegram/WhatsApp group ID)", }, name: { type: "string", @@ -69,6 +67,10 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { type: "string", description: "Path to git repo (e.g. '~/git/my-project')", }, + channel: { + type: "string", + description: "Channel type (e.g. 'telegram', 'whatsapp'). Defaults to 'telegram'.", + }, groupName: { type: "string", description: "Group display name (optional - defaults to 'Project: {name}')", @@ -97,6 +99,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { const groupId = params.projectGroupId as string; const name = params.name as string; const repo = params.repo as string; + const channel = (params.channel as string) ?? "telegram"; const groupName = (params.groupName as string) ?? `Project: ${name}`; const baseBranch = params.baseBranch as string; const deployBranch = (params.deployBranch as string) ?? baseBranch; @@ -108,43 +111,9 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { throw new Error("No workspace directory available in tool context"); } - // --- Context detection --- - const devClawAgentIds = - ((api.pluginConfig as Record)?.devClawAgentIds as - | string[] - | undefined) ?? []; - const context = await detectContext(ctx, devClawAgentIds); - - // ONLY allow registration from group context - // Design principle: One Group = One Project = One Team - // This enforces project isolation and prevents accidental cross-registration. - // You must be IN the group to register it, making the binding explicit and intentional. - if (context.type !== "group") { - return jsonResult({ - success: false, - error: "Project registration can only be done from the Telegram/WhatsApp group you're registering.", - recommendation: - context.type === "via-agent" - ? "If you're setting up DevClaw for the first time, use onboard. Then go to the project's Telegram/WhatsApp group to register it." - : "Please go to the Telegram/WhatsApp group you want to register and call project_register from there.", - contextGuidance: generateGuardrails(context), - }); - } - - // Auto-populate projectGroupId if not provided (use current group) - const actualGroupId = groupId || ctx.sessionKey; - if (!actualGroupId) { - throw new Error("Could not determine group ID from context. Please provide projectGroupId explicitly."); - } - - // Provide helpful note if project is already registered - const contextInfo = context.projectName - ? `Note: This group is already registered as "${context.projectName}". You may be re-registering it.` - : `Registering project for this ${context.channel} group (ID: ${actualGroupId.substring(0, 20)}...).`; - // 1. Check project not already registered (allow re-register if incomplete) const data = await readProjects(workspaceDir); - const existing = data.projects[actualGroupId]; + const existing = data.projects[groupId]; if (existing && existing.dev?.sessions && Object.keys(existing.dev.sessions).length > 0) { throw new Error( `Project already registered for this group: "${existing.name}". Remove the existing entry first or use a different group.`, @@ -160,8 +129,8 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { const healthy = await provider.healthCheck(); if (!healthy) { const cliName = providerType === "github" ? "gh" : "glab"; - const cliInstallUrl = providerType === "github" - ? "https://cli.github.com" + const cliInstallUrl = providerType === "github" + ? "https://cli.github.com" : "https://gitlab.com/gitlab-org/cli"; throw new Error( `${providerType.toUpperCase()} health check failed for ${repoPath}. ` + @@ -176,14 +145,14 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { await provider.ensureAllStateLabels(); // 5. Add project to projects.json - data.projects[actualGroupId] = { + data.projects[groupId] = { name, repo, groupName, deployUrl, baseBranch, deployBranch, - channel: context.channel, + channel, roleExecution, dev: emptyWorkerState([...DEV_LEVELS]), qa: emptyWorkerState([...QA_LEVELS]), @@ -197,7 +166,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { // 7. Audit log await auditLog(workspaceDir, "project_register", { project: name, - groupId: actualGroupId, + groupId, repo, baseBranch, deployBranch, @@ -206,20 +175,18 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) { // 8. Return announcement const promptsNote = promptsCreated ? " Prompt files scaffolded." : ""; - const announcement = `📋 Project "${name}" registered for group ${groupName}. Labels created.${promptsNote} Ready for tasks.`; + const announcement = `Project "${name}" registered for group ${groupName}. Labels created.${promptsNote} Ready for tasks.`; return jsonResult({ success: true, project: name, - groupId: actualGroupId, + groupId, repo, baseBranch, deployBranch, labelsCreated: 8, promptsScaffolded: promptsCreated, announcement, - ...(contextInfo && { contextInfo }), - contextGuidance: generateGuardrails(context), }); }, }); diff --git a/lib/tools/status.ts b/lib/tools/status.ts index b5edc79..85d38cc 100644 --- a/lib/tools/status.ts +++ b/lib/tools/status.ts @@ -3,22 +3,20 @@ * * Shows worker state and queue counts per project. No health checks * (use `health` tool), no complex sequencing. - * Context-aware: auto-filters to project in group chats. */ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { jsonResult } from "openclaw/plugin-sdk"; import type { ToolContext } from "../types.js"; import { readProjects, getProject } from "../projects.js"; -import { generateGuardrails } from "../context-guard.js"; import { log as auditLog } from "../audit.js"; import { fetchProjectQueues, type QueueLabel } from "../services/queue.js"; -import { requireWorkspaceDir, resolveContext, getPluginConfig } from "../tool-helpers.js"; +import { requireWorkspaceDir, getPluginConfig } from "../tool-helpers.js"; export function createStatusTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ name: "status", label: "Status", - description: `Show task queue and worker state per project. Context-aware: auto-filters in group chats. Use \`health\` tool for worker health checks.`, + description: `Show task queue and worker state per project. Use \`health\` tool for worker health checks.`, parameters: { type: "object", properties: { @@ -28,20 +26,7 @@ export function createStatusTool(api: OpenClawPluginApi) { async execute(_id: string, params: Record) { const workspaceDir = requireWorkspaceDir(ctx); - - const context = await resolveContext(ctx, api); - if (context.type === "via-agent") { - return jsonResult({ - success: false, - warning: "status is for operational use, not setup.", - recommendation: "Use onboard instead for DevClaw setup.", - contextGuidance: generateGuardrails(context), - }); - } - - // Auto-filter in group context - let groupId = params.projectGroupId as string | undefined; - if (context.type === "group" && !groupId) groupId = context.groupId; + const groupId = params.projectGroupId as string | undefined; const pluginConfig = getPluginConfig(api); const projectExecution = (pluginConfig?.projectExecution as string) ?? "parallel"; @@ -80,11 +65,6 @@ export function createStatusTool(api: OpenClawPluginApi) { success: true, execution: { projectExecution }, projects: filtered, - context: { - type: context.type, - ...(context.type === "group" && { projectName: context.projectName, autoFiltered: !params.projectGroupId }), - }, - contextGuidance: generateGuardrails(context), }); }, }); diff --git a/lib/tools/work-start.ts b/lib/tools/work-start.ts index 2c1a5e7..765af7d 100644 --- a/lib/tools/work-start.ts +++ b/lib/tools/work-start.ts @@ -15,7 +15,7 @@ import { dispatchTask } from "../dispatch.js"; import { notify, getNotificationConfig } from "../notify.js"; import { findNextIssue, detectRoleFromLabel, detectLevelFromLabels } from "../services/tick.js"; import { isDevLevel } from "../tiers.js"; -import { requireWorkspaceDir, resolveContext, resolveProject, resolveProvider, groupOnlyError, getPluginConfig, tickAndNotify } from "../tool-helpers.js"; +import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js"; export function createWorkStartTool(api: OpenClawPluginApi) { return (ctx: ToolContext) => ({ @@ -24,10 +24,11 @@ export function createWorkStartTool(api: OpenClawPluginApi) { description: `Pick up a task from the issue queue. ONLY works in project group chats. Handles label transition, level assignment, session creation, dispatch, and audit. Picks up only the explicitly requested issue.`, parameters: { type: "object", + required: ["projectGroupId"], properties: { + projectGroupId: { type: "string", description: "Project group ID." }, issueId: { type: "number", description: "Issue ID. If omitted, picks next by priority." }, role: { type: "string", enum: ["dev", "qa"], description: "Worker role. Auto-detected from label if omitted." }, - projectGroupId: { type: "string", description: "Project group ID. Auto-detected from group context." }, level: { type: "string", description: "Developer level (junior/medior/senior/reviewer). Auto-detected if omitted." }, }, }, @@ -35,15 +36,11 @@ export function createWorkStartTool(api: OpenClawPluginApi) { async execute(_id: string, params: Record) { const issueIdParam = params.issueId as number | undefined; const roleParam = params.role as "dev" | "qa" | undefined; - const groupIdParam = params.projectGroupId as string | undefined; + const groupId = params.projectGroupId as string; const levelParam = (params.level ?? params.tier) as string | undefined; const workspaceDir = requireWorkspaceDir(ctx); - // Context guard: group only - const context = await resolveContext(ctx, api); - if (context.type !== "group") return groupOnlyError("work_start", context); - - const groupId = groupIdParam ?? context.groupId; + if (!groupId) throw new Error("projectGroupId is required"); const { project } = await resolveProject(workspaceDir, groupId); const { provider } = resolveProvider(project); @@ -107,7 +104,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) { const notifyConfig = getNotificationConfig(pluginConfig); await notify( { type: "workerStart", project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title, issueUrl: issue.web_url, role, level: dr.level, sessionAction: dr.sessionAction }, - { workspaceDir, config: notifyConfig, groupId, channel: context.channel }, + { workspaceDir, config: notifyConfig, groupId, channel: project.channel ?? "telegram" }, ); // Auto-tick disabled per issue #125 - work_start should only pick up the explicitly requested issue @@ -118,7 +115,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) { role, level: dr.level, model: dr.model, sessionAction: dr.sessionAction, announcement: dr.announcement, labelTransition: `${currentLabel} → ${targetLabel}`, levelReason, levelSource, - autoDetected: { projectGroupId: !groupIdParam, role: !roleParam, issueId: issueIdParam === undefined, level: !levelParam }, + autoDetected: { role: !roleParam, issueId: issueIdParam === undefined, level: !levelParam }, }; // tickPickups removed with auto-tick diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 5f9e07d..39c8d87 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -5,11 +5,6 @@ "configSchema": { "type": "object", "properties": { - "devClawAgentIds": { - "type": "array", - "items": { "type": "string" }, - "description": "Agent IDs recognized as DevClaw orchestrators" - }, "models": { "type": "object", "description": "Model mapping per role and tier", @@ -40,9 +35,8 @@ }, "notifications": { "type": "object", - "description": "Notification settings", + "description": "Per-event-type notification toggles. All default to true — set to false to suppress.", "properties": { - "heartbeatDm": { "type": "boolean" }, "workerStart": { "type": "boolean" }, "workerComplete": { "type": "boolean" } }