feat: cw task add CLI command + {AGENT_ID} prompt placeholder
- Add `createTaskForAgent` tRPC mutation: resolves agent → task → phase, creates sibling task
- Add `cw task add <name> --agent-id <id>` CLI command
- Replace `{AGENT_ID}` and `{AGENT_NAME}` placeholders in writeInputFiles() before flushing
- Update docs/agent.md and docs/cli-config.md
This commit is contained in:
@@ -282,6 +282,17 @@ export async function writeInputFiles(options: WriteInputFilesOptions): Promise<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace agent placeholders in all content before writing
|
||||||
|
const placeholders: Record<string, string> = {
|
||||||
|
'{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
|
// 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')));
|
await Promise.all(writes.map(w => writeFile(w.path, w.content, 'utf-8')));
|
||||||
|
|
||||||
|
|||||||
@@ -426,6 +426,32 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cw task add <name> --agent-id <agentId>
|
||||||
|
taskCommand
|
||||||
|
.command('add <name>')
|
||||||
|
.description('Create a sibling task in the agent\'s current phase')
|
||||||
|
.requiredOption('--agent-id <agentId>', 'Agent ID creating the task')
|
||||||
|
.option('--description <description>', 'Task description')
|
||||||
|
.option('--category <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
|
// Message command group
|
||||||
const messageCommand = program
|
const messageCommand = program
|
||||||
.command('message')
|
.command('message')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
requirePhaseRepository,
|
requirePhaseRepository,
|
||||||
requireDispatchManager,
|
requireDispatchManager,
|
||||||
requireChangeSetRepository,
|
requireChangeSetRepository,
|
||||||
|
requireAgentManager,
|
||||||
} from './_helpers.js';
|
} from './_helpers.js';
|
||||||
|
|
||||||
export function taskProcedures(publicProcedure: ProcedureBuilder) {
|
export function taskProcedures(publicProcedure: ProcedureBuilder) {
|
||||||
@@ -213,5 +214,55 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
return edges;
|
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',
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
1. **tRPC procedure** calls `agentManager.spawn(options)`
|
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).
|
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/<alias>/<project>/`. After creation, each project subdirectory is verified to exist; missing worktrees throw immediately to prevent agents running in the wrong directory.
|
3. `AgentProcessManager.createProjectWorktrees()` — creates git worktrees at `agent-workdirs/<alias>/<project>/`. 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()`
|
5. Provider config builds spawn command via `buildSpawnCommand()`
|
||||||
6. `spawnDetached()` — launches detached child process with file output redirection
|
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
|
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 `<examples>` > `<example label="good">` /
|
|||||||
### Execute Prompt Dispatch
|
### Execute Prompt Dispatch
|
||||||
|
|
||||||
`buildExecutePrompt(taskDescription?)` accepts an optional task description wrapped in a `<task>` 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 `<workspace>` and `<inter_agent_communication>` blocks are appended by the agent manager at spawn time.
|
`buildExecutePrompt(taskDescription?)` accepts an optional task description wrapped in a `<task>` 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 `<workspace>` and `<inter_agent_communication>` 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}`"*
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ Uses **Commander.js** for command parsing.
|
|||||||
| `list --parent\|--phase\|--initiative <id>` | List tasks with counts |
|
| `list --parent\|--phase\|--initiative <id>` | List tasks with counts |
|
||||||
| `get <taskId>` | Task details |
|
| `get <taskId>` | Task details |
|
||||||
| `status <taskId> <status>` | Update status |
|
| `status <taskId> <status>` | Update status |
|
||||||
|
| `add <name> --agent-id <id> [--description <desc>] [--category <cat>]` | Create sibling task in agent's phase |
|
||||||
|
|
||||||
### Dispatch (`cw dispatch`)
|
### Dispatch (`cw dispatch`)
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
Reference in New Issue
Block a user