refactor: remove context awareness documentation and related code; streamline tool registration and context detection
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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:**
|
||||
|
||||
|
||||
4
index.ts
4
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"] });
|
||||
|
||||
@@ -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<InteractionContext> {
|
||||
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<string | undefined> {
|
||||
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<string, { name: string }>;
|
||||
};
|
||||
|
||||
// groupId IS the key in the Record
|
||||
return data.projects[groupId]?.name;
|
||||
} catch {
|
||||
// File doesn't exist or parse error
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, string>; qa: Record<string, string> };
|
||||
|
||||
/**
|
||||
* 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<string, unknown>): void {
|
||||
(defaults.subagents as Record<string, unknown>).archiveAfterMinutes = 43200;
|
||||
}
|
||||
|
||||
function addDevClawAgentId(config: Record<string, unknown>, 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<string, unknown>, agentId: string): void {
|
||||
const agent = (config as any).agents?.list?.find((a: { id: string }) => a.id === agentId);
|
||||
if (agent) {
|
||||
|
||||
@@ -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<InteractionContext> {
|
||||
const devClawAgentIds =
|
||||
((api.pluginConfig as Record<string, unknown>)?.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).
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
const devClawAgentIds = ((api.pluginConfig as Record<string, unknown>)?.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<string, unknown>);
|
||||
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),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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<string, unknown>)?.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),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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
|
||||
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user