Skip to content

Commit 1b798d0

Browse files
Support project-level approval from the CLI
1 parent 2903e89 commit 1b798d0

File tree

6 files changed

+260
-10
lines changed

6 files changed

+260
-10
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,16 +213,18 @@ The TaskManager now uses a direct tools interface with specific, purpose-built t
213213
Tasks have a status field that can be one of:
214214
- `not started`: Task has not been started yet
215215
- `in progress`: Task is currently being worked on
216-
- `done`: Task has been completed
216+
- `done`: Task has been completed (requires `completedDetails`)
217217

218218
#### Status Transition Rules
219+
219220
The system enforces the following rules for task status transitions:
220221
- Tasks follow a specific workflow with defined valid transitions:
221222
- From `not started`: Can only move to `in progress`
222223
- From `in progress`: Can move to either `done` or back to `not started`
223224
- From `done`: Can move back to `in progress` if additional work is needed
224-
- When a task is marked as "done", the `completedDetails` field should be provided to document what was completed
225+
- When a task is marked as "done", the `completedDetails` field must be provided to document what was completed
225226
- Approved tasks cannot be modified
227+
- A project can only be approved when all tasks are both done and approved
226228

227229
These rules help maintain the integrity of task progress and ensure proper documentation of completed work.
228230

@@ -243,7 +245,7 @@ A typical workflow for an LLM using this task manager would be:
243245

244246
#### Task Approval
245247

246-
Task approval is controlled exclusively by the human user through a CLI command:
248+
Task approval is controlled exclusively by the human user through the CLI command:
247249

248250
```bash
249251
npm run approve-task -- <projectId> <taskId>
@@ -252,7 +254,7 @@ npm run approve-task -- <projectId> <taskId>
252254
Options:
253255
- `-f, --force`: Force approval even if the task is not marked as done
254256

255-
This command sets the `approved` field of a task to `true` after verifying that the task is marked as `done`. Only the human user can approve tasks, ensuring quality control.
257+
Note: Tasks must be marked as "done" with completed details before they can be approved (unless using --force).
256258

257259
#### Listing Tasks and Projects
258260

@@ -342,7 +344,7 @@ const nextTaskResult = await toolManager.callFunction('get_next_task', {
342344
const markDoneResult = await toolManager.callFunction('mark_task_done', {
343345
projectId: "proj-1234",
344346
taskId: "task-1",
345-
completedDetails: "Created repository at github.com/example/business-site and initialized with HTML5 boilerplate, CSS reset, and basic JS structure."
347+
completedDetails: "Created repository at github.com/example/business-site and initialized with HTML5 boilerplate, CSS reset, and basic JS structure." // Required when marking as done
346348
});
347349

348350
// Response will include:

src/cli.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,92 @@ program
150150
}
151151
});
152152

153+
program
154+
.command("approve-project")
155+
.description("Approve project completion")
156+
.argument("<projectId>", "ID of the project to approve")
157+
.action(async (projectId) => {
158+
try {
159+
console.log(chalk.blue(`Approving project ${chalk.bold(projectId)}...`));
160+
161+
const data = await readData();
162+
163+
// Check if we have any projects
164+
if (data.projects.length === 0) {
165+
console.error(chalk.red(`No projects found. The task file is empty or just initialized.`));
166+
process.exit(1);
167+
}
168+
169+
const project = data.projects.find(p => p.projectId === projectId);
170+
171+
if (!project) {
172+
console.error(chalk.red(`Project ${chalk.bold(projectId)} not found.`));
173+
console.log(chalk.yellow('Available projects:'));
174+
data.projects.forEach(p => {
175+
console.log(` - ${p.projectId}: ${p.initialPrompt.substring(0, 50)}${p.initialPrompt.length > 50 ? '...' : ''}`);
176+
});
177+
process.exit(1);
178+
}
179+
180+
// Check if all tasks are done & approved
181+
const allDone = project.tasks.every(t => t.status === "done");
182+
if (!allDone) {
183+
console.error(chalk.red(`Not all tasks in project ${chalk.bold(projectId)} are marked as done.`));
184+
console.log(chalk.yellow('\nPending tasks:'));
185+
project.tasks.filter(t => t.status !== "done").forEach(t => {
186+
console.log(` - ${chalk.bold(t.id)}: ${t.title} (Status: ${t.status})`);
187+
});
188+
process.exit(1);
189+
}
190+
191+
const allApproved = project.tasks.every(t => t.approved);
192+
if (!allApproved) {
193+
console.error(chalk.red(`Not all tasks in project ${chalk.bold(projectId)} are approved yet.`));
194+
console.log(chalk.yellow('\nUnapproved tasks:'));
195+
project.tasks.filter(t => !t.approved).forEach(t => {
196+
console.log(` - ${chalk.bold(t.id)}: ${t.title}`);
197+
});
198+
process.exit(1);
199+
}
200+
201+
if (project.completed) {
202+
console.log(chalk.yellow(`Project ${chalk.bold(projectId)} is already approved and completed.`));
203+
process.exit(0);
204+
}
205+
206+
project.completed = true;
207+
await writeData(data);
208+
console.log(chalk.green(`✅ Project ${chalk.bold(projectId)} has been approved and marked as complete.`));
209+
210+
// Show project info
211+
console.log(chalk.cyan('\n📋 Project details:'));
212+
console.log(` - ${chalk.bold('Initial Prompt:')} ${project.initialPrompt}`);
213+
if (project.projectPlan && project.projectPlan !== project.initialPrompt) {
214+
console.log(` - ${chalk.bold('Project Plan:')} ${project.projectPlan}`);
215+
}
216+
console.log(` - ${chalk.bold('Status:')} ${chalk.green('Completed ✓')}`);
217+
218+
// Show progress info
219+
const totalTasks = project.tasks.length;
220+
const completedTasks = project.tasks.filter(t => t.status === "done").length;
221+
const approvedTasks = project.tasks.filter(t => t.approved).length;
222+
223+
console.log(chalk.cyan(`\n📊 Final Progress: ${chalk.bold(`${approvedTasks}/${completedTasks}/${totalTasks}`)} (approved/completed/total)`));
224+
225+
// Create a progress bar
226+
const bar = '▓'.repeat(approvedTasks) + '▒'.repeat(completedTasks - approvedTasks) + '░'.repeat(totalTasks - completedTasks);
227+
console.log(` ${bar}`);
228+
229+
console.log(chalk.green('\n🎉 Project successfully completed and approved!'));
230+
console.log(chalk.gray('You can view the project details anytime using:'));
231+
console.log(chalk.blue(` task-manager-cli list -p ${projectId}`));
232+
233+
} catch (error) {
234+
console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`));
235+
process.exit(1);
236+
}
237+
});
238+
153239
program
154240
.command("list")
155241
.description("List all projects and their tasks")

src/server/TaskManagerServer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ export class TaskManagerServer {
267267
const proj = this.data.projects.find((p) => p.projectId === projectId);
268268
if (!proj) return { status: "error", message: "Project not found" };
269269

270+
// Check if project is already completed
271+
if (proj.completed) {
272+
return { status: "error", message: "Project is already completed." };
273+
}
274+
270275
// Check if all tasks are done and approved
271276
const allDone = proj.tasks.every((t) => t.status === "done");
272277
if (!allDone) {

src/types/tools.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const readProjectTool: Tool = {
3232
// Create Project
3333
const createProjectTool: Tool = {
3434
name: "create_project",
35-
description: "Create a new project with an initial prompt and a list of tasks.",
35+
description: "Create a new project with an initial prompt and a list of tasks. This is typically the first step in any workflow.",
3636
inputSchema: {
3737
type: "object",
3838
properties: {
@@ -120,7 +120,7 @@ const addTasksToProjectTool: Tool = {
120120
// Finalize Project (Mark as Complete)
121121
const finalizeProjectTool: Tool = {
122122
name: "finalize_project",
123-
description: "Mark a project as complete after all tasks are done and approved.",
123+
description: "Mark a project as complete. Can only be called when all tasks are both done and approved. This is typically the last step in a project workflow.",
124124
inputSchema: {
125125
type: "object",
126126
properties: {
@@ -199,7 +199,7 @@ const createTaskTool: Tool = {
199199
// Update Task
200200
const updateTaskTool: Tool = {
201201
name: "update_task",
202-
description: "Modify a task's title, description, or status. Note: completedDetails are required when setting status to 'done'.",
202+
description: "Modify a task's properties. Note: (1) completedDetails are required when setting status to 'done', (2) approved tasks cannot be modified, (3) status must follow valid transitions: not started → in progress → done.",
203203
inputSchema: {
204204
type: "object",
205205
properties: {
@@ -256,7 +256,7 @@ const deleteTaskTool: Tool = {
256256
// Approve Task
257257
const approveTaskTool: Tool = {
258258
name: "approve_task",
259-
description: "Approve a completed task.",
259+
description: "Approve a completed task. Tasks must be marked as 'done' with completedDetails before approval. Note: This is a CLI-only operation that requires human intervention.",
260260
inputSchema: {
261261
type: "object",
262262
properties: {
@@ -276,7 +276,7 @@ const approveTaskTool: Tool = {
276276
// Get Next Task
277277
const getNextTaskTool: Tool = {
278278
name: "get_next_task",
279-
description: "Get the next task to be done in a project.",
279+
description: "Get the next task to be done in a project. Returns the first non-approved task in sequence, regardless of status. Use this to determine which task to work on next.",
280280
inputSchema: {
281281
type: "object",
282282
properties: {

tests/integration/server.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,70 @@ describe('TaskManagerServer Integration', () => {
302302
const project = server["data"].projects.find(p => p.projectId === projectId);
303303
expect(project?.completed).toBe(true);
304304
});
305+
306+
it('should handle project approval workflow', async () => {
307+
// 1. Create a project with multiple tasks
308+
const createResult = await server.createProject(
309+
'Project for approval workflow',
310+
[
311+
{
312+
title: 'Task 1',
313+
description: 'Description of task 1'
314+
},
315+
{
316+
title: 'Task 2',
317+
description: 'Description of task 2'
318+
}
319+
]
320+
) as {
321+
projectId: string;
322+
tasks: { id: string }[];
323+
};
324+
325+
const projectId = createResult.projectId;
326+
const taskId1 = createResult.tasks[0].id;
327+
const taskId2 = createResult.tasks[1].id;
328+
329+
// 2. Try to approve project before tasks are done (should fail)
330+
const earlyApprovalResult = await server.approveProjectCompletion(projectId);
331+
expect(earlyApprovalResult.status).toBe('error');
332+
expect(earlyApprovalResult.message).toContain('Not all tasks are done');
333+
334+
// 3. Mark tasks as done
335+
await server.markTaskDone(projectId, taskId1, 'Task 1 completed details');
336+
await server.markTaskDone(projectId, taskId2, 'Task 2 completed details');
337+
338+
// 4. Try to approve project before tasks are approved (should fail)
339+
const preApprovalResult = await server.approveProjectCompletion(projectId);
340+
expect(preApprovalResult.status).toBe('error');
341+
expect(preApprovalResult.message).toContain('Not all done tasks are approved');
342+
343+
// 5. Approve tasks
344+
await server.approveTaskCompletion(projectId, taskId1);
345+
await server.approveTaskCompletion(projectId, taskId2);
346+
347+
// 6. Now approve the project (should succeed)
348+
const approvalResult = await server.approveProjectCompletion(projectId);
349+
expect(approvalResult.status).toBe('project_approved_complete');
350+
351+
// 7. Verify project state
352+
const project = server["data"].projects.find(p => p.projectId === projectId);
353+
expect(project?.completed).toBe(true);
354+
expect(project?.tasks.every(t => t.status === 'done')).toBe(true);
355+
expect(project?.tasks.every(t => t.approved)).toBe(true);
356+
357+
// 8. Try to approve again (should fail)
358+
const reapprovalResult = await server.approveProjectCompletion(projectId);
359+
expect(reapprovalResult.status).toBe('error');
360+
expect(reapprovalResult.message).toContain('Project is already completed');
361+
362+
// 9. Verify project is still listed
363+
const listResult = await server.listProjects();
364+
const listedProject = listResult.projects?.find(p => p.projectId === projectId);
365+
expect(listedProject).toBeDefined();
366+
expect(listedProject?.initialPrompt).toBe('Project for approval workflow');
367+
expect(listedProject?.totalTasks).toBe(2);
368+
expect(listedProject?.completedTasks).toBe(2);
369+
expect(listedProject?.approvedTasks).toBe(2);
370+
});
305371
});

tests/unit/TaskManagerServer.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,95 @@ describe('TaskManagerServer', () => {
327327
expect(task?.status).toBe('done');
328328
});
329329
});
330+
331+
describe('Project Approval', () => {
332+
let projectId: string;
333+
let taskId1: string;
334+
let taskId2: string;
335+
336+
beforeEach(async () => {
337+
// Create a project with two tasks for each test in this group
338+
const createResult = await server.createProject(
339+
'Test project for approval',
340+
[
341+
{
342+
title: 'Task 1',
343+
description: 'Description for task 1'
344+
},
345+
{
346+
title: 'Task 2',
347+
description: 'Description for task 2'
348+
}
349+
]
350+
) as {
351+
projectId: string;
352+
tasks: { id: string }[];
353+
};
354+
355+
projectId = createResult.projectId;
356+
taskId1 = createResult.tasks[0].id;
357+
taskId2 = createResult.tasks[1].id;
358+
});
359+
360+
it('should not approve project if tasks are not done', async () => {
361+
const result = await server.approveProjectCompletion(projectId);
362+
expect(result.status).toBe('error');
363+
expect(result.message).toContain('Not all tasks are done');
364+
});
365+
366+
it('should not approve project if tasks are done but not approved', async () => {
367+
// Mark both tasks as done
368+
const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1);
369+
const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2);
370+
if (task1 && task2) {
371+
task1.status = 'done';
372+
task2.status = 'done';
373+
await server["saveTasks"]();
374+
}
375+
376+
const result = await server.approveProjectCompletion(projectId);
377+
expect(result.status).toBe('error');
378+
expect(result.message).toContain('Not all done tasks are approved');
379+
});
380+
381+
it('should approve project when all tasks are done and approved', async () => {
382+
// Mark both tasks as done and approved
383+
const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1);
384+
const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2);
385+
if (task1 && task2) {
386+
task1.status = 'done';
387+
task2.status = 'done';
388+
task1.approved = true;
389+
task2.approved = true;
390+
await server["saveTasks"]();
391+
}
392+
393+
const result = await server.approveProjectCompletion(projectId);
394+
expect(result.status).toBe('project_approved_complete');
395+
396+
// Verify project is marked as completed
397+
const project = server["data"].projects.find(p => p.projectId === projectId);
398+
expect(project?.completed).toBe(true);
399+
});
400+
401+
it('should not allow approving an already completed project', async () => {
402+
// First approve the project
403+
const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1);
404+
const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2);
405+
if (task1 && task2) {
406+
task1.status = 'done';
407+
task2.status = 'done';
408+
task1.approved = true;
409+
task2.approved = true;
410+
await server["saveTasks"]();
411+
}
412+
413+
await server.approveProjectCompletion(projectId);
414+
415+
// Try to approve again
416+
const result = await server.approveProjectCompletion(projectId);
417+
expect(result.status).toBe('error');
418+
expect(result.message).toContain('Project is already completed');
419+
});
420+
});
330421
});

0 commit comments

Comments
 (0)