From e1991886704167378f01b51e4feb3d02a6783235 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 6 Mar 2026 22:22:49 +0100 Subject: [PATCH] feat: `cw task add` CLI command + `{AGENT_ID}` prompt placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `createTaskForAgent` tRPC mutation: resolves agent → task → phase, creates sibling task - Add `cw task add --agent-id ` CLI command - Replace `{AGENT_ID}` and `{AGENT_NAME}` placeholders in writeInputFiles() before flushing - Update docs/agent.md and docs/cli-config.md --- apps/server/agent/file-io.ts | 11 +++++++ apps/server/cli/index.ts | 26 ++++++++++++++++ apps/server/trpc/routers/task.ts | 51 ++++++++++++++++++++++++++++++++ docs/agent.md | 13 +++++++- docs/cli-config.md | 1 + 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/apps/server/agent/file-io.ts b/apps/server/agent/file-io.ts index 4bbc296..4b26116 100644 --- a/apps/server/agent/file-io.ts +++ b/apps/server/agent/file-io.ts @@ -282,6 +282,17 @@ export async function writeInputFiles(options: WriteInputFilesOptions): Promise< }); } + // Replace agent placeholders in all content before writing + const placeholders: Record = { + '{AGENT_ID}': options.agentId ?? '', + '{AGENT_NAME}': options.agentName ?? '', + }; + for (const w of writes) { + for (const [token, value] of Object.entries(placeholders)) { + w.content = w.content.replaceAll(token, value); + } + } + // Flush all file writes in parallel — yields the event loop between I/O ops await Promise.all(writes.map(w => writeFile(w.path, w.content, 'utf-8'))); diff --git a/apps/server/cli/index.ts b/apps/server/cli/index.ts index 1e37383..c0ef879 100644 --- a/apps/server/cli/index.ts +++ b/apps/server/cli/index.ts @@ -426,6 +426,32 @@ export function createCli(serverHandler?: (port?: number) => Promise): Com } }); + // cw task add --agent-id + taskCommand + .command('add ') + .description('Create a sibling task in the agent\'s current phase') + .requiredOption('--agent-id ', 'Agent ID creating the task') + .option('--description ', 'Task description') + .option('--category ', 'Task category (execute, research, verify, ...)') + .action(async (name: string, options: { agentId: string; description?: string; category?: string }) => { + try { + const client = createDefaultTrpcClient(); + const task = await client.createTaskForAgent.mutate({ + agentId: options.agentId, + name, + description: options.description, + category: options.category as 'execute' | undefined, + }); + console.log(`Created task: ${task.name}`); + console.log(` ID: ${task.id}`); + console.log(` Phase: ${task.phaseId}`); + console.log(` Status: ${task.status}`); + } catch (error) { + console.error('Failed to create task:', (error as Error).message); + process.exit(1); + } + }); + // Message command group const messageCommand = program .command('message') diff --git a/apps/server/trpc/routers/task.ts b/apps/server/trpc/routers/task.ts index 44f0e95..d7d3641 100644 --- a/apps/server/trpc/routers/task.ts +++ b/apps/server/trpc/routers/task.ts @@ -11,6 +11,7 @@ import { requirePhaseRepository, requireDispatchManager, requireChangeSetRepository, + requireAgentManager, } from './_helpers.js'; export function taskProcedures(publicProcedure: ProcedureBuilder) { @@ -213,5 +214,55 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { return edges; }), + createTaskForAgent: publicProcedure + .input(z.object({ + agentId: z.string().min(1), + name: z.string().min(1), + description: z.string().optional(), + category: z.enum(['execute', 'research', 'discuss', 'plan', 'detail', 'refine', 'verify', 'merge', 'review']).optional(), + })) + .mutation(async ({ ctx, input }) => { + const agentManager = requireAgentManager(ctx); + const taskRepository = requireTaskRepository(ctx); + + const agent = await agentManager.get(input.agentId); + if (!agent) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Agent '${input.agentId}' not found`, + }); + } + if (!agent.taskId) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Agent '${agent.name}' has no assigned task`, + }); + } + + const agentTask = await taskRepository.findById(agent.taskId); + if (!agentTask) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Agent's task '${agent.taskId}' not found`, + }); + } + if (!agentTask.phaseId) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `Agent's task has no phase — cannot create sibling task`, + }); + } + + return taskRepository.create({ + phaseId: agentTask.phaseId, + initiativeId: agentTask.initiativeId, + name: input.name, + description: input.description ?? null, + category: input.category ?? 'execute', + type: 'auto', + status: 'pending', + }); + }), + }; } diff --git a/docs/agent.md b/docs/agent.md index 15503bf..cba1530 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -33,7 +33,7 @@ 1. **tRPC procedure** calls `agentManager.spawn(options)` 2. Manager generates alias (adjective-animal), creates DB record. Appends inter-agent communication and preview instructions unless `skipPromptExtras: true` (used by conflict-resolution agents to keep prompts lean). 3. `AgentProcessManager.createProjectWorktrees()` — creates git worktrees at `agent-workdirs///`. After creation, each project subdirectory is verified to exist; missing worktrees throw immediately to prevent agents running in the wrong directory. -4. `file-io.writeInputFiles()` — writes `.cw/input/` with assignment files (initiative, pages, phase, task) and read-only context dirs (`context/phases/`, `context/tasks/`) +4. `file-io.writeInputFiles()` — writes `.cw/input/` with assignment files (initiative, pages, phase, task) and read-only context dirs (`context/phases/`, `context/tasks/`). Before flushing writes, replaces `{AGENT_ID}` and `{AGENT_NAME}` placeholders in all content with the agent's real ID and name. 5. Provider config builds spawn command via `buildSpawnCommand()` 6. `spawnDetached()` — launches detached child process with file output redirection 7. `FileTailer` watches output file, fires `onEvent` (parsed stream events) and `onRawContent` (raw JSONL chunks) callbacks @@ -271,3 +271,14 @@ Examples within mode-specific tags use `` > `` / ### Execute Prompt Dispatch `buildExecutePrompt(taskDescription?)` accepts an optional task description wrapped in a `` tag. The dispatch manager (`apps/server/dispatch/manager.ts`) wraps `task.description || task.name` in `buildExecutePrompt()` so execute agents receive full system context alongside their task. The `` and `` blocks are appended by the agent manager at spawn time. + +## Prompt Placeholders + +`writeInputFiles()` replaces these tokens in all input file content before writing: + +| Placeholder | Replaced With | +|-------------|---------------| +| `{AGENT_ID}` | The agent's database ID | +| `{AGENT_NAME}` | The agent's human-readable name | + +Use in phase detail text or task descriptions to give agents self-referential context, e.g.: *"Report issues via `cw task add --agent-id {AGENT_ID}`"* diff --git a/docs/cli-config.md b/docs/cli-config.md index a249d64..b1b42b6 100644 --- a/docs/cli-config.md +++ b/docs/cli-config.md @@ -44,6 +44,7 @@ Uses **Commander.js** for command parsing. | `list --parent\|--phase\|--initiative ` | List tasks with counts | | `get ` | Task details | | `status ` | Update status | +| `add --agent-id [--description ] [--category ]` | Create sibling task in agent's phase | ### Dispatch (`cw dispatch`) | Command | Description |