| name | pm-organization-task-selection |
| description | Priority algorithm for selecting next PRD task based on category, dependencies, and risk |
| category | organization |
Task Selection Skill
"Fail fast on risky work โ tackle hard problems before easy wins."
Parallel Task Opportunity Check (MANDATORY FIRST STEP)
โ ๏ธ CRITICAL: BEFORE selecting any single task, ALWAYS check if parallel assignment is possible.
When both Developer AND Tech Artist are idle, the PM MUST check for parallel work opportunities. This happens BEFORE the standard single-task selection flow.
Check Steps
const prd = readJson('prd.json');
const backlog = readJson(prd.backlogFile || 'prd_backlog.json');
const allItems = [...prd.items, ...backlog.backlogItems];
const developerIdle =
prd.agents.developer?.status === 'idle' || prd.agents.developer?.status === 'awaiting_pm';
const techartistIdle =
prd.agents.techartist?.status === 'idle' || prd.agents.techartist?.status === 'awaiting_pm';
if (developerIdle && techartistIdle) {
const devCategories = ['architectural', 'functional', 'integration'];
const devTasks = allItems
.filter(
(item) =>
!item.passes &&
item.status === 'pending' &&
devCategories.includes(item.category) &&
item.dependencies.every((depId) => allItems.find((i) => i.id === depId)?.passes === true)
)
.sort(priorityComparator);
const artistCategories = ['visual', 'shader', 'polish'];
const artistTasks = allItems
.filter(
(item) =>
!item.passes &&
item.status === 'pending' &&
artistCategories.includes(item.category) &&
item.dependencies.every((depId) => allItems.find((i) => i.id === depId)?.passes === true)
)
.sort(priorityComparator);
if (devTasks.length > 0 && artistTasks.length > 0) {
const devTask = devTasks[0];
const artistTask = artistTasks[0];
if (!areTasksConflicting(devTask, artistTask)) {
assignTask(devTask, 'developer');
assignTask(artistTask, 'techartist');
return 'parallel_assigned';
}
}
}
Conflict Detection Function
function areTasksConflicting(task1, task2) {
const paths1 = task1.files || [];
const paths2 = task2.files || [];
for (const p1 of paths1) {
for (const p2 of paths2) {
const dir1 = p1.substring(0, p1.lastIndexOf('/'));
const dir2 = p2.substring(0, p2.lastIndexOf('/'));
if (dir1 === dir2) return true;
}
}
const deps1 = new Set(task1.dependencies || []);
const deps2 = new Set(task2.dependencies || []);
for (const dep of deps1) {
if (deps2.has(dep)) return true;
}
return false;
}
When to Use This Skill
Use when:
- Assigning a new task from the PRD
prd.json.session.currentTask is null or task status is "passed"
- After retrospective completes
โ ๏ธ FIRST check for parallel assignment, then proceed to single-task selection if parallel is not possible.
Quick Start
const prd = readJson('prd.json');
const backlog = readJson('prd.backlogFile' || 'prd_backlog.json');
const allItems = [...prd.items, ...backlog.backlogItems];
const incomplete = allItems.filter((item) => !item.passes);
const unblocked = incomplete.filter((item) =>
item.dependencies.every((depId) => allItems.find((i) => i.id === depId)?.passes === true)
);
const next = unblocked.sort(priorityComparator)[0];
PRD Backlog Architecture
Tasks are split between two files:
| File | Contains | Size |
|---|
prd.json | Top 5 active queue | ~5 tasks |
prd_backlog.json | Remaining backlog | ~70 tasks |
Only PM and Game Designer need full backlog access. Workers only read their assigned task.
Reading Tasks from Both Files
When selecting tasks, PM must:
- Read
prd.json for active queue
- Read
prd.backlogFile (defaults to prd_backlog.json)
- Combine arrays:
allItems = [...prd.items, ...backlog.backlogItems]
- Apply filtering/sorting on combined array
- If selected task is in backlog, move it to
prd.json.items
Automatic Backlog Refill
After a task completes (status โ "completed") and is removed from active queue:
if (prd.items.length < 5) {
const backlog = readJson(prd.backlogFile);
const allTasks = [...prd.items, ...backlog.backlogItems];
const candidates = backlog.backlogItems.filter((item) => {
if (item.passes) return false;
const depsMet = item.dependencies.every(
(depId) => allTasks.find((t) => t.id === depId)?.passes === true
);
return depsMet;
});
const tierOrder = {
TIER_0_BLOCKER: 1,
TIER_1_FOUNDATION: 2,
TIER_2_ECONOMY: 3,
TIER_3_SUPPORT: 4,
};
candidates.sort((a, b) => {
const tierDiff = (tierOrder[a.tier] || 99) - (tierOrder[b.tier] || 99);
if (tierDiff !== 0) return tierDiff;
return (
(a.priority === 'critical' ? 1 : a.priority === 'high' ? 2 : 3) -
(b.priority === 'critical' ? 1 : b.priority === 'high' ? 2 : 3)
);
});
if (candidates.length > 0) {
const toMove = candidates[0];
backlog.backlogItems = backlog.backlogItems.filter((i) => i.id !== toMove.id);
prd.items.push(toMove);
prd.session.stats.backlogSize = backlog.backlogItems.length;
prd.session.stats.activeQueueSize = prd.items.length;
writeJson('prd_backlog.json', backlog);
writeJson('prd.json', prd);
}
}
Decision Framework
| Category | Priority | When to Use |
|---|
architectural | 1 (Highest) | Affects entire codebase structure |
integration | 2 | Reveals incompatibilities early |
spike / unknown | 3 | Exploratory work, reduces uncertainty |
functional | 4 | Standard feature implementation |
polish | 5 (Lowest) | UI, optimization, documentation |
Progressive Guide
Level 1: Basic Selection
Select first incomplete, unblocked task (reads from both files):
const prd = readJson('prd.json');
const backlog = readJson(prd.backlogFile || 'prd_backlog.json');
const allItems = [...prd.items, ...backlog.backlogItems];
const next = allItems.find(
(item) => !item.passes && item.dependencies.every((d) => allItems.find((i) => i.id === d)?.passes)
);
Level 2: Priority-Weighted Selection
Apply category priority ordering (reads from both files):
const prd = readJson('prd.json');
const backlog = readJson(prd.backlogFile || 'prd_backlog.json');
const allItems = [...prd.items, ...backlog.backlogItems];
const priorityOrder = {
architectural: 1,
integration: 2,
unknown: 3,
spike: 3,
functional: 4,
polish: 5,
};
const priorityValue = { high: 1, medium: 2, low: 3 };
const incomplete = allItems.filter((item) => !item.passes);
const unblocked = incomplete.filter((item) =>
item.dependencies.every((d) => allItems.find((i) => i.id === d)?.passes)
);
unblocked.sort((a, b) => {
const catDiff = priorityOrder[a.category] - priorityOrder[b.category];
if (catDiff !== 0) return catDiff;
return priorityValue[a.priority] - priorityValue[b.priority];
});
Level 3: Risk-Adjusted Selection
Consider retry count and complexity (reads from both files):
const prd = readJson('prd.json');
const backlog = readJson(prd.backlogFile || 'prd_backlog.json');
const allItems = [...prd.items, ...backlog.backlogItems];
if (task.retryCount >= 3) {
logWarning(`Task ${task.id} failed ${task.retryCount} times`);
}
const unblockScore = allItems.filter((i) => i.dependencies.includes(task.id)).length;
Anti-Patterns
โ DON'T:
- Select tasks with unmet dependencies
- Assign new task while
prd.json.session.currentTask is not null
- Skip retrospective to assign faster
- Assign multiple tasks to the SAME agent simultaneously
- Assign parallel tasks that modify the same files/directories
- Skip backlog refill when
prd.json.items.length < 5
- Only read prd.json without checking prd_backlog.json
- Leave completed tasks in prd.json instead of moving to completed file
โ
DO:
- Verify all dependencies have
passes: true (check across both files)
- Wait for QA validation before assigning next
- Complete retrospective before clearing
prd.json.session.currentTask
- Log selection rationale in progress file
- Assign parallel tasks to Developer AND Tech Artist when safe (see Parallel Assignment below)
- Read both prd.json and prd_backlog.json when selecting tasks
- Automatically refill when active queue drops below 5 tasks
- Move completed tasks to prd_completed.json and refill queue
Parallel Task Assignment (Git Worktree Support)
With git worktrees, Developer and Tech Artist can work simultaneously on non-conflicting tasks.
When to Assign Parallel Tasks
Check these conditions before assigning parallel tasks:
-
Both agents are idle:
prd.json.agents.developer.status === "idle" (or "awaiting_pm")
prd.json.agents.techartist.status === "idle" (or "awaiting_pm")
-
Tasks are non-conflicting:
- Different file paths (e.g.,
src/hooks/ vs src/assets/)
- No shared dependencies
- Can be tested independently
-
Each agent has their own worktree:
- Developer works in
../developer-worktree on developer-worktree branch
- Tech Artist works in
../techartist-worktree on techartist-worktree branch
Conflict Detection Matrix
| Developer Category | Tech Artist Category | File Path Overlap? | Safe for Parallel? |
|---|
architectural (src/hooks, src/stores, src/server) | visual (src/assets) | โ No | โ
Yes |
functional (src/components/* logic) | shader (src/vfx) | โ No | โ
Yes |
integration (src/utils) | polish (src/styles) | โ No | โ
Yes |
architectural (src/components) | visual (src/components) | โ ๏ธ Yes | โ No - sequential only |
functional (public/) | visual (public/) | โ ๏ธ Yes | โ No - sequential only |
Parallel Assignment Algorithm
const developerIdle = prd.agents.developer?.status === 'idle';
const techartistIdle = prd.agents.techartist?.status === 'idle';
if (developerIdle && techartistIdle) {
const devTask = findNextTaskForAgent('developer', allItems);
const artistTask = findNextTaskForAgent('techartist', allItems);
if (areTasksNonConflicting(devTask, artistTask)) {
assignTask(devTask, 'developer');
assignTask(artistTask, 'techartist');
return;
}
}
File Path-Based Conflict Detection
function areTasksNonConflicting(task1, task2) {
const devPaths = [
'src/hooks/',
'src/stores/',
'src/server/',
'src/utils/',
'src/types/',
'test/',
];
const artistPaths = [
'src/assets/',
'src/vfx/',
'src/styles/',
'public/textures/',
'public/models/',
];
const task1Path = guessFilePathFromTask(task1);
const task2Path = guessFilePathFromTask(task2);
const task1InDev = devPaths.some((p) => task1Path?.startsWith(p));
const task2InArtist = artistPaths.some((p) => task2Path?.startsWith(p));
return task1InDev && task2InArtist;
}
Parallel Assignment Steps
- Check both Developer and Tech Artist are idle
- Find highest-priority incomplete task for each agent
- Verify tasks are non-conflicting (different file paths, no shared deps)
- Assign to Developer (5-step atomic process)
- Assign to Tech Artist (5-step atomic process)
- Exit - both agents work in parallel in their worktrees
Checklist
Before assigning a task:
โ ๏ธ CRITICAL: New Phased Workflow Must Complete Before Assignment
When a task status is "passed", the following phases MUST complete in order:
- Retrospective (worker contributions only) โ
retrospective_synthesized
- Playtest Session (Game Designer validates) โ
playtest_complete
- PRD Refinement (optional, if needed) โ
prd_refinement or skip
- Acceptance Criteria (MANDATORY - Game Designer provides) โ
task_ready
- Skill Research (PM improves ALL skills) โ
completed
ONLY when status is "completed" may you set prd.json.session.currentTask = null and select the next task.
โ ๏ธ NEVER assign a task without acceptance criteria from Game Designer.
After Selecting a Task
Once a task is selected, you MUST update it in the PRD before assigning:
- Read
prd.json
- Find the selected task by ID
- Update the task with:
{
"status": "assigned",
"assignedAt": "{{ISO_TIMESTAMP}}"
}
- Write back to
prd.json
- Then update session state in
prd.json.session:
{
"session": {
"currentTask": "{{taskId}}",
"currentTaskStatus": "assigned",
"lastAssignmentTime": "{{ISO_TIMESTAMP}}"
}
}
โ ๏ธ This is Step 1 of the 5-step atomic assignment process. See PM AGENT.md for complete flow.
Completed Task Cleanup (MANDATORY After Retrospective)
โ ๏ธ CRITICAL: After a task completes retrospective (status: "completed"), you MUST:
- Move completed task to
prd_completed.json
- Remove it from
prd.json.items array
- Refill from backlog if
prd.json.items.length < 5
Step-by-Step Cleanup Procedure
const prd = readJson('prd.json');
const completedTasks = prd.items.filter(
(item) => item.status === 'completed' && item.passes === true
);
for (const task of completedTasks) {
const entry = {
id: task.id,
title: task.title,
category: task.category,
tier: task.tier,
completedAt: task.completedAt || task.qaValidatedAt,
notes: task.notes || '',
validationSummary: task.validationResults || 'PASSED',
};
appendFileSync('prd_completed.json', JSON.stringify(entry, null, 2) + '\n\n');
}
prd.items = prd.items.filter((item) => !(item.status === 'completed' && item.passes === true));
prd.completedTasks =
parseInt(readFileSync('prd_completed.json', 'utf8').match(/"id":/g)?.length || 0) +
completedTasks.length;
if (prd.items.length < 5) {
const backlog = readJson(prd.backlogFile || 'prd_backlog.json');
const allTasks = [...prd.items, ...backlog.backlogItems];
const candidates = backlog.backlogItems.filter((item) => {
if (item.passes || item.status === 'deferred') return false;
const depsMet =
item.dependencies?.every((depId) => allTasks.find((t) => t.id === depId)?.passes === true) ??
true;
return depsMet;
});
const tierOrder = {
TIER_0_BLOCKER: 1,
TIER_1_FOUNDATION: 2,
TIER_2_ECONOMY: 3,
TIER_3_SUPPORT: 4,
};
candidates.sort((a, b) => {
const tierDiff = (tierOrder[a.tier] || 99) - (tierOrder[b.tier] || 99);
if (tierDiff !== 0) return tierDiff;
return (
(a.priority === 'critical' ? 1 : a.priority === 'high' ? 2 : 3) -
(b.priority === 'critical' ? 1 : b.priority === 'high' ? 2 : 3)
);
});
while (prd.items.length < 5 && candidates.length > 0) {
const toMove = candidates.shift();
backlog.backlogItems = backlog.backlogItems.filter((i) => i.id !== toMove.id);
prd.items.push(toMove);
}
writeJson(prd.backlogFile || 'prd_backlog.json', backlog);
}
prd.session.stats.activeQueueSize = prd.items.length;
prd.session.stats.completed = prd.completedTasks;
prd.session.stats.backlogSize = backlog.backlogItems.length;
writeJson('prd.json', prd);
Cleanup Timing
When to run cleanup:
- โ
AFTER retrospective completes (task status = "completed", passes = true)
- โ
BEFORE selecting next task (keeps prd.json clean)
- โ NOT during task assignment (too many file operations)
Cleanup Checklist
Reference
Data-Driven Task Selection
JSON-Based Level Design Tasks
When assigning tasks related to JSON level data:
Verification Requirements:
- Level JSON files must have valid schema
- Coordinates must be within world bounds
- Material/pig/bird types must be valid enums
- Star thresholds should follow progressive formula
- Multiple solution paths must exist
Test Plan Considerations:
- Unit tests: Schema validation with Ajv
- E2E tests: Load and verify level data
- Manual tests: Play through each level
Priority Adjustment for Data Tasks
| Task Type | Base Priority | Adjustment |
|---|
| Level JSON creation | functional | +1 if blocks progression |
| Schema validation | architectural | +2 (data integrity) |
| Asset loading | architectural | +1 (blocks gameplay) |