|
|
|
@ -5,63 +5,63 @@ const processStore = require('./processStore.js');
|
|
|
|
const { wrapAgentBlock } = require('./agentBlocks.js');
|
|
|
|
const { wrapAgentBlock } = require('./agentBlocks.js');
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeActorName(value) {
|
|
|
|
function normalizeActorName(value) {
|
|
|
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
|
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSameMember(left, right) {
|
|
|
|
function isSameMember(left, right) {
|
|
|
|
return normalizeActorName(left).toLowerCase() === normalizeActorName(right).toLowerCase();
|
|
|
|
return normalizeActorName(left).toLowerCase() === normalizeActorName(right).toLowerCase();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSameTaskMember(left, right, leadName) {
|
|
|
|
function isSameTaskMember(left, right, leadName) {
|
|
|
|
const normalizedLeft = normalizeActorName(left).toLowerCase();
|
|
|
|
const normalizedLeft = normalizeActorName(left).toLowerCase();
|
|
|
|
const normalizedRight = normalizeActorName(right).toLowerCase();
|
|
|
|
const normalizedRight = normalizeActorName(right).toLowerCase();
|
|
|
|
const normalizedLead = normalizeActorName(leadName).toLowerCase();
|
|
|
|
const normalizedLead = normalizeActorName(leadName).toLowerCase();
|
|
|
|
if (!normalizedLeft || !normalizedRight) {
|
|
|
|
if (!normalizedLeft || !normalizedRight) {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (normalizedLeft === normalizedRight) {
|
|
|
|
if (normalizedLeft === normalizedRight) {
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
(normalizedLeft === 'team-lead' && normalizedRight === normalizedLead) ||
|
|
|
|
(normalizedLeft === 'team-lead' && normalizedRight === normalizedLead) ||
|
|
|
|
(normalizedRight === 'team-lead' && normalizedLeft === normalizedLead)
|
|
|
|
(normalizedRight === 'team-lead' && normalizedLeft === normalizedLead)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function quoteMarkdown(text) {
|
|
|
|
function quoteMarkdown(text) {
|
|
|
|
return String(text)
|
|
|
|
return String(text)
|
|
|
|
.split('\n')
|
|
|
|
.split('\n')
|
|
|
|
.map((line) => `> ${line}`)
|
|
|
|
.map((line) => `> ${line}`)
|
|
|
|
.join('\n');
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildAssignmentMessage(context, task, options = {}) {
|
|
|
|
function buildAssignmentMessage(context, task, options = {}) {
|
|
|
|
const description =
|
|
|
|
const description =
|
|
|
|
typeof options.description === 'string' && options.description.trim()
|
|
|
|
typeof options.description === 'string' && options.description.trim() ?
|
|
|
|
? options.description.trim()
|
|
|
|
options.description.trim() :
|
|
|
|
: typeof task.description === 'string' && task.description.trim()
|
|
|
|
typeof task.description === 'string' && task.description.trim() ?
|
|
|
|
? task.description.trim()
|
|
|
|
task.description.trim() :
|
|
|
|
: '';
|
|
|
|
'';
|
|
|
|
const prompt =
|
|
|
|
const prompt =
|
|
|
|
typeof options.prompt === 'string' && options.prompt.trim() ? options.prompt.trim() : '';
|
|
|
|
typeof options.prompt === 'string' && options.prompt.trim() ? options.prompt.trim() : '';
|
|
|
|
const taskLabel = `#${task.displayId || task.id}`;
|
|
|
|
const taskLabel = `#${task.displayId || task.id}`;
|
|
|
|
const lines = [
|
|
|
|
const lines = [
|
|
|
|
`New task assigned to you: ${taskLabel} "${task.subject}".`,
|
|
|
|
`New task assigned to you: ${taskLabel} "${task.subject}".`,
|
|
|
|
``,
|
|
|
|
``,
|
|
|
|
`*If you are idle and this task is ready to start, start it now. If you are busy, blocked, or still need more context, immediately add a short task comment with the reason and your best ETA or what you are waiting on, and keep this task in TODO until you actually begin.*`,
|
|
|
|
`*If you are idle and this task is ready to start, start it now. If you are busy, blocked, or still need more context, immediately add a short task comment with the reason and your best ETA or what you are waiting on, and keep this task in TODO until you actually begin.*`,
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (description) {
|
|
|
|
if (description) {
|
|
|
|
lines.push(``, `Description:`, description);
|
|
|
|
lines.push(``, `Description:`, description);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (prompt) {
|
|
|
|
if (prompt) {
|
|
|
|
lines.push(``, `Instructions:`, prompt);
|
|
|
|
lines.push(``, `Instructions:`, prompt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
lines.push(
|
|
|
|
``,
|
|
|
|
``,
|
|
|
|
wrapAgentBlock(`Use the board MCP tools to work this task correctly:
|
|
|
|
wrapAgentBlock(`Use the board MCP tools to work this task correctly:
|
|
|
|
1. Check the latest full context before starting:
|
|
|
|
1. Check the latest full context before starting:
|
|
|
|
task_get { teamName: "${context.teamName}", taskId: "${task.id}" }
|
|
|
|
task_get { teamName: "${context.teamName}", taskId: "${task.id}" }
|
|
|
|
2. If you are idle and the task is ready to start after checking dependencies and context, call task_start now:
|
|
|
|
2. If you are idle and the task is ready to start after checking dependencies and context, call task_start now:
|
|
|
|
@ -70,304 +70,301 @@ function buildAssignmentMessage(context, task, options = {}) {
|
|
|
|
task_add_comment { teamName: "${context.teamName}", taskId: "${task.id}", text: "<reason + ETA or blocker>", from: "<your-name>" }
|
|
|
|
task_add_comment { teamName: "${context.teamName}", taskId: "${task.id}", text: "<reason + ETA or blocker>", from: "<your-name>" }
|
|
|
|
4. When the work is done, mark it completed:
|
|
|
|
4. When the work is done, mark it completed:
|
|
|
|
task_complete { teamName: "${context.teamName}", taskId: "${task.id}" }`)
|
|
|
|
task_complete { teamName: "${context.teamName}", taskId: "${task.id}" }`)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
return lines.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildCommentNotificationMessage(context, task, comment) {
|
|
|
|
function buildCommentNotificationMessage(context, task, comment) {
|
|
|
|
const taskLabel = `#${task.displayId || task.id}`;
|
|
|
|
const taskLabel = `#${task.displayId || task.id}`;
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
`**Comment on task ${taskLabel}**`,
|
|
|
|
`**Comment on task ${taskLabel}** _${task.subject}_`,
|
|
|
|
`> ${task.subject}`,
|
|
|
|
``,
|
|
|
|
``,
|
|
|
|
quoteMarkdown(comment.text),
|
|
|
|
quoteMarkdown(comment.text),
|
|
|
|
``,
|
|
|
|
``,
|
|
|
|
wrapAgentBlock(`Reply to this comment using MCP tool task_add_comment:
|
|
|
|
wrapAgentBlock(`Reply to this comment using MCP tool task_add_comment:
|
|
|
|
|
|
|
|
{ teamName: "${context.teamName}", taskId: "${task.id}", text: "<your reply>", from: "<your-name>" }`),
|
|
|
|
{ teamName: "${context.teamName}", taskId: "${task.id}", text: "<your reply>", from: "<your-name>" }`),
|
|
|
|
].join('\n');
|
|
|
|
].join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function maybeNotifyAssignedOwner(context, task, options = {}) {
|
|
|
|
function maybeNotifyAssignedOwner(context, task, options = {}) {
|
|
|
|
const owner = normalizeActorName(task.owner);
|
|
|
|
const owner = normalizeActorName(task.owner);
|
|
|
|
if (!owner || task.status === 'deleted') {
|
|
|
|
if (!owner || task.status === 'deleted') {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
const sender = normalizeActorName(options.from) || leadName;
|
|
|
|
const sender = normalizeActorName(options.from) || leadName;
|
|
|
|
const leadSessionId = runtimeHelpers.resolveLeadSessionId(context.paths);
|
|
|
|
const leadSessionId = runtimeHelpers.resolveLeadSessionId(context.paths);
|
|
|
|
if (isSameMember(owner, leadName) || isSameMember(owner, sender)) {
|
|
|
|
if (isSameMember(owner, leadName) || isSameMember(owner, sender)) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const summary = options.summary || `New task #${task.displayId || task.id} assigned`;
|
|
|
|
const summary = options.summary || `New task #${task.displayId || task.id} assigned`;
|
|
|
|
messages.sendMessage(context, {
|
|
|
|
messages.sendMessage(context, {
|
|
|
|
member: owner,
|
|
|
|
member: owner,
|
|
|
|
from: sender,
|
|
|
|
from: sender,
|
|
|
|
text: buildAssignmentMessage(context, task, options),
|
|
|
|
text: buildAssignmentMessage(context, task, options),
|
|
|
|
taskRefs: Array.isArray(options.taskRefs) && options.taskRefs.length > 0 ? options.taskRefs : undefined,
|
|
|
|
taskRefs: Array.isArray(options.taskRefs) && options.taskRefs.length > 0 ? options.taskRefs : undefined,
|
|
|
|
summary,
|
|
|
|
summary,
|
|
|
|
source: 'system_notification',
|
|
|
|
source: 'system_notification',
|
|
|
|
...(leadSessionId ? { leadSessionId } : {}),
|
|
|
|
...(leadSessionId ? { leadSessionId } : {}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function maybeNotifyTaskOwnerOnComment(context, task, comment, options = {}) {
|
|
|
|
function maybeNotifyTaskOwnerOnComment(context, task, comment, options = {}) {
|
|
|
|
if (!options.inserted || options.notifyOwner === false) {
|
|
|
|
if (!options.inserted || options.notifyOwner === false) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!task || task.status === 'deleted') {
|
|
|
|
if (!task || task.status === 'deleted') {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (comment.type && comment.type !== 'regular') {
|
|
|
|
if (comment.type && comment.type !== 'regular') {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const owner = normalizeActorName(task.owner);
|
|
|
|
const owner = normalizeActorName(task.owner);
|
|
|
|
if (!owner) {
|
|
|
|
if (!owner) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
if (isSameTaskMember(owner, comment.author, leadName)) {
|
|
|
|
if (isSameTaskMember(owner, comment.author, leadName)) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const leadSessionId = runtimeHelpers.resolveLeadSessionId(context.paths);
|
|
|
|
const leadSessionId = runtimeHelpers.resolveLeadSessionId(context.paths);
|
|
|
|
messages.sendMessage(context, {
|
|
|
|
messages.sendMessage(context, {
|
|
|
|
member: owner,
|
|
|
|
member: owner,
|
|
|
|
from: normalizeActorName(comment.author) || leadName,
|
|
|
|
from: normalizeActorName(comment.author) || leadName,
|
|
|
|
text: buildCommentNotificationMessage(context, task, comment),
|
|
|
|
text: buildCommentNotificationMessage(context, task, comment),
|
|
|
|
taskRefs: Array.isArray(comment.taskRefs) ? comment.taskRefs : undefined,
|
|
|
|
taskRefs: Array.isArray(comment.taskRefs) ? comment.taskRefs : undefined,
|
|
|
|
summary: `Comment on #${task.displayId || task.id}`,
|
|
|
|
summary: `Comment on #${task.displayId || task.id}`,
|
|
|
|
source: 'system_notification',
|
|
|
|
source: 'system_notification',
|
|
|
|
...(leadSessionId ? { leadSessionId } : {}),
|
|
|
|
...(leadSessionId ? { leadSessionId } : {}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createTask(context, input) {
|
|
|
|
function createTask(context, input) {
|
|
|
|
const task = taskStore.createTask(context.paths, input);
|
|
|
|
const task = taskStore.createTask(context.paths, input);
|
|
|
|
if (input && input.notifyOwner !== false) {
|
|
|
|
if (input && input.notifyOwner !== false) {
|
|
|
|
maybeNotifyAssignedOwner(context, task, {
|
|
|
|
maybeNotifyAssignedOwner(context, task, {
|
|
|
|
description: input.description,
|
|
|
|
description: input.description,
|
|
|
|
prompt: input.prompt,
|
|
|
|
prompt: input.prompt,
|
|
|
|
taskRefs: [
|
|
|
|
taskRefs: [
|
|
|
|
...(Array.isArray(input.descriptionTaskRefs) ? input.descriptionTaskRefs : []),
|
|
|
|
...(Array.isArray(input.descriptionTaskRefs) ? input.descriptionTaskRefs : []),
|
|
|
|
...(Array.isArray(input.promptTaskRefs) ? input.promptTaskRefs : []),
|
|
|
|
...(Array.isArray(input.promptTaskRefs) ? input.promptTaskRefs : []),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
from: input.from,
|
|
|
|
from: input.from,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return task;
|
|
|
|
return task;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTask(context, taskId) {
|
|
|
|
function getTask(context, taskId) {
|
|
|
|
return taskStore.readTask(context.paths, taskId, { includeDeleted: true });
|
|
|
|
return taskStore.readTask(context.paths, taskId, { includeDeleted: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function listTasks(context) {
|
|
|
|
function listTasks(context) {
|
|
|
|
return taskStore.listTasks(context.paths);
|
|
|
|
return taskStore.listTasks(context.paths);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function listDeletedTasks(context) {
|
|
|
|
function listDeletedTasks(context) {
|
|
|
|
return taskStore.listTasks(context.paths, { includeDeleted: true }).filter(
|
|
|
|
return taskStore.listTasks(context.paths, { includeDeleted: true }).filter(
|
|
|
|
(task) => task.status === 'deleted'
|
|
|
|
(task) => task.status === 'deleted'
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTaskId(context, taskRef) {
|
|
|
|
function resolveTaskId(context, taskRef) {
|
|
|
|
return taskStore.resolveTaskRef(context.paths, taskRef, { includeDeleted: true });
|
|
|
|
return taskStore.resolveTaskRef(context.paths, taskRef, { includeDeleted: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setTaskStatus(context, taskId, status, actor) {
|
|
|
|
function setTaskStatus(context, taskId, status, actor) {
|
|
|
|
return taskStore.setTaskStatus(context.paths, taskId, status, actor);
|
|
|
|
return taskStore.setTaskStatus(context.paths, taskId, status, actor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startTask(context, taskId, actor) {
|
|
|
|
function startTask(context, taskId, actor) {
|
|
|
|
const task = setTaskStatus(context, taskId, 'in_progress', actor);
|
|
|
|
const task = setTaskStatus(context, taskId, 'in_progress', actor);
|
|
|
|
// Clear stale kanban entry (e.g. 'approved' or 'review') when task is reopened
|
|
|
|
// Clear stale kanban entry (e.g. 'approved' or 'review') when task is reopened
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const kanbanStore = require('./kanbanStore.js');
|
|
|
|
const kanbanStore = require('./kanbanStore.js');
|
|
|
|
const state = kanbanStore.readKanbanState(context.paths, context.teamName);
|
|
|
|
const state = kanbanStore.readKanbanState(context.paths, context.teamName);
|
|
|
|
if (state.tasks[task.id]) {
|
|
|
|
if (state.tasks[task.id]) {
|
|
|
|
delete state.tasks[task.id];
|
|
|
|
delete state.tasks[task.id];
|
|
|
|
kanbanStore.writeKanbanState(context.paths, context.teamName, state);
|
|
|
|
kanbanStore.writeKanbanState(context.paths, context.teamName, state);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
// Best-effort: task status already updated, kanban cleanup failure is non-fatal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
return task;
|
|
|
|
// Best-effort: task status already updated, kanban cleanup failure is non-fatal
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return task;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function completeTask(context, taskId, actor) {
|
|
|
|
function completeTask(context, taskId, actor) {
|
|
|
|
return setTaskStatus(context, taskId, 'completed', actor);
|
|
|
|
return setTaskStatus(context, taskId, 'completed', actor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function softDeleteTask(context, taskId, actor) {
|
|
|
|
function softDeleteTask(context, taskId, actor) {
|
|
|
|
return setTaskStatus(context, taskId, 'deleted', actor);
|
|
|
|
return setTaskStatus(context, taskId, 'deleted', actor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function restoreTask(context, taskId, actor) {
|
|
|
|
function restoreTask(context, taskId, actor) {
|
|
|
|
return setTaskStatus(context, taskId, 'pending', actor || 'user');
|
|
|
|
return setTaskStatus(context, taskId, 'pending', actor || 'user');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setTaskOwner(context, taskId, owner) {
|
|
|
|
function setTaskOwner(context, taskId, owner) {
|
|
|
|
const previousTask = taskStore.readTask(context.paths, taskId, { includeDeleted: true });
|
|
|
|
const previousTask = taskStore.readTask(context.paths, taskId, { includeDeleted: true });
|
|
|
|
const updatedTask = taskStore.setTaskOwner(context.paths, taskId, owner);
|
|
|
|
const updatedTask = taskStore.setTaskOwner(context.paths, taskId, owner);
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
if (
|
|
|
|
owner != null &&
|
|
|
|
owner != null &&
|
|
|
|
normalizeActorName(updatedTask.owner) &&
|
|
|
|
normalizeActorName(updatedTask.owner) &&
|
|
|
|
!isSameMember(previousTask.owner, updatedTask.owner)
|
|
|
|
!isSameMember(previousTask.owner, updatedTask.owner)
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
maybeNotifyAssignedOwner(context, updatedTask, {
|
|
|
|
maybeNotifyAssignedOwner(context, updatedTask, {
|
|
|
|
summary: `Task #${updatedTask.displayId || updatedTask.id} assigned`,
|
|
|
|
summary: `Task #${updatedTask.displayId || updatedTask.id} assigned`,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return updatedTask;
|
|
|
|
return updatedTask;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateTaskFields(context, taskId, fields) {
|
|
|
|
function updateTaskFields(context, taskId, fields) {
|
|
|
|
return taskStore.updateTaskFields(context.paths, taskId, fields);
|
|
|
|
return taskStore.updateTaskFields(context.paths, taskId, fields);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addTaskComment(context, taskId, flags) {
|
|
|
|
function addTaskComment(context, taskId, flags) {
|
|
|
|
const result = taskStore.addTaskComment(context.paths, taskId, flags.text, {
|
|
|
|
const result = taskStore.addTaskComment(context.paths, taskId, flags.text, {
|
|
|
|
author:
|
|
|
|
author: typeof flags.from === 'string' && flags.from.trim() ?
|
|
|
|
typeof flags.from === 'string' && flags.from.trim()
|
|
|
|
flags.from.trim() : runtimeHelpers.inferLeadName(context.paths),
|
|
|
|
? flags.from.trim()
|
|
|
|
...(flags.id ? { id: flags.id } : {}),
|
|
|
|
: runtimeHelpers.inferLeadName(context.paths),
|
|
|
|
...(flags.createdAt ? { createdAt: flags.createdAt } : {}),
|
|
|
|
...(flags.id ? { id: flags.id } : {}),
|
|
|
|
...(flags.type ? { type: flags.type } : {}),
|
|
|
|
...(flags.createdAt ? { createdAt: flags.createdAt } : {}),
|
|
|
|
...(Array.isArray(flags.taskRefs) ? { taskRefs: flags.taskRefs } : {}),
|
|
|
|
...(flags.type ? { type: flags.type } : {}),
|
|
|
|
...(Array.isArray(flags.attachments) ? { attachments: flags.attachments } : {}),
|
|
|
|
...(Array.isArray(flags.taskRefs) ? { taskRefs: flags.taskRefs } : {}),
|
|
|
|
|
|
|
|
...(Array.isArray(flags.attachments) ? { attachments: flags.attachments } : {}),
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
maybeNotifyTaskOwnerOnComment(context, result.task, result.comment, {
|
|
|
|
|
|
|
|
inserted: result.inserted,
|
|
|
|
|
|
|
|
notifyOwner: flags.notifyOwner,
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (notifyError) {
|
|
|
|
|
|
|
|
// Best-effort: comment is already persisted, notification failure must not fail the call
|
|
|
|
|
|
|
|
if (typeof console !== 'undefined' && console.warn) {
|
|
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
|
|
`[tasks] owner notification failed for task ${taskId}: ${String(notifyError)}`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
try {
|
|
|
|
commentId: result.comment.id,
|
|
|
|
maybeNotifyTaskOwnerOnComment(context, result.task, result.comment, {
|
|
|
|
taskId: result.task.id,
|
|
|
|
inserted: result.inserted,
|
|
|
|
subject: result.task.subject,
|
|
|
|
notifyOwner: flags.notifyOwner,
|
|
|
|
owner: result.task.owner,
|
|
|
|
});
|
|
|
|
task: result.task,
|
|
|
|
} catch (notifyError) {
|
|
|
|
comment: result.comment,
|
|
|
|
// Best-effort: comment is already persisted, notification failure must not fail the call
|
|
|
|
};
|
|
|
|
if (typeof console !== 'undefined' && console.warn) {
|
|
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
|
|
`[tasks] owner notification failed for task ${taskId}: ${String(notifyError)}`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
commentId: result.comment.id,
|
|
|
|
|
|
|
|
taskId: result.task.id,
|
|
|
|
|
|
|
|
subject: result.task.subject,
|
|
|
|
|
|
|
|
owner: result.task.owner,
|
|
|
|
|
|
|
|
task: result.task,
|
|
|
|
|
|
|
|
comment: result.comment,
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function attachTaskFile(context, taskId, flags) {
|
|
|
|
function attachTaskFile(context, taskId, flags) {
|
|
|
|
const canonicalTaskId = resolveTaskId(context, taskId);
|
|
|
|
const canonicalTaskId = resolveTaskId(context, taskId);
|
|
|
|
const saved = runtimeHelpers.saveTaskAttachmentFile(context.paths, canonicalTaskId, flags);
|
|
|
|
const saved = runtimeHelpers.saveTaskAttachmentFile(context.paths, canonicalTaskId, flags);
|
|
|
|
const task = taskStore.addTaskAttachmentMeta(context.paths, canonicalTaskId, saved.meta);
|
|
|
|
const task = taskStore.addTaskAttachmentMeta(context.paths, canonicalTaskId, saved.meta);
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
...saved.meta,
|
|
|
|
...saved.meta,
|
|
|
|
task,
|
|
|
|
task,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function attachCommentFile(context, taskId, commentId, flags) {
|
|
|
|
function attachCommentFile(context, taskId, commentId, flags) {
|
|
|
|
const canonicalTaskId = resolveTaskId(context, taskId);
|
|
|
|
const canonicalTaskId = resolveTaskId(context, taskId);
|
|
|
|
const saved = runtimeHelpers.saveTaskAttachmentFile(context.paths, canonicalTaskId, flags);
|
|
|
|
const saved = runtimeHelpers.saveTaskAttachmentFile(context.paths, canonicalTaskId, flags);
|
|
|
|
const task = taskStore.addCommentAttachmentMeta(context.paths, canonicalTaskId, commentId, saved.meta);
|
|
|
|
const task = taskStore.addCommentAttachmentMeta(context.paths, canonicalTaskId, commentId, saved.meta);
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
...saved.meta,
|
|
|
|
...saved.meta,
|
|
|
|
task,
|
|
|
|
task,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addTaskAttachmentMeta(context, taskId, meta) {
|
|
|
|
function addTaskAttachmentMeta(context, taskId, meta) {
|
|
|
|
return taskStore.addTaskAttachmentMeta(context.paths, taskId, meta);
|
|
|
|
return taskStore.addTaskAttachmentMeta(context.paths, taskId, meta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeTaskAttachment(context, taskId, attachmentId) {
|
|
|
|
function removeTaskAttachment(context, taskId, attachmentId) {
|
|
|
|
return taskStore.removeTaskAttachment(context.paths, taskId, attachmentId);
|
|
|
|
return taskStore.removeTaskAttachment(context.paths, taskId, attachmentId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setNeedsClarification(context, taskId, value) {
|
|
|
|
function setNeedsClarification(context, taskId, value) {
|
|
|
|
return taskStore.setNeedsClarification(context.paths, taskId, value == null ? 'clear' : String(value));
|
|
|
|
return taskStore.setNeedsClarification(context.paths, taskId, value == null ? 'clear' : String(value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function linkTask(context, taskId, targetId, linkType) {
|
|
|
|
function linkTask(context, taskId, targetId, linkType) {
|
|
|
|
return taskStore.linkTask(context.paths, taskId, targetId, String(linkType));
|
|
|
|
return taskStore.linkTask(context.paths, taskId, targetId, String(linkType));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function unlinkTask(context, taskId, targetId, linkType) {
|
|
|
|
function unlinkTask(context, taskId, targetId, linkType) {
|
|
|
|
return taskStore.unlinkTask(context.paths, taskId, targetId, String(linkType));
|
|
|
|
return taskStore.unlinkTask(context.paths, taskId, targetId, String(linkType));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function taskBriefing(context, memberName) {
|
|
|
|
async function taskBriefing(context, memberName) {
|
|
|
|
return taskStore.formatTaskBriefing(context.paths, context.teamName, String(memberName));
|
|
|
|
return taskStore.formatTaskBriefing(context.paths, context.teamName, String(memberName));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSystemLocale() {
|
|
|
|
function getSystemLocale() {
|
|
|
|
const lang = typeof process.env.LANG === 'string' ? process.env.LANG.trim() : '';
|
|
|
|
const lang = typeof process.env.LANG === 'string' ? process.env.LANG.trim() : '';
|
|
|
|
if (!lang) return 'en';
|
|
|
|
if (!lang) return 'en';
|
|
|
|
return lang.split('.')[0].replace('_', '-');
|
|
|
|
return lang.split('.')[0].replace('_', '-');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractPrimaryLanguage(locale) {
|
|
|
|
function extractPrimaryLanguage(locale) {
|
|
|
|
const normalized = String(locale || '').trim();
|
|
|
|
const normalized = String(locale || '').trim();
|
|
|
|
const dash = normalized.indexOf('-');
|
|
|
|
const dash = normalized.indexOf('-');
|
|
|
|
return dash > 0 ? normalized.slice(0, dash) : normalized || 'en';
|
|
|
|
return dash > 0 ? normalized.slice(0, dash) : normalized || 'en';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveLanguageName(code, systemLocale) {
|
|
|
|
function resolveLanguageName(code, systemLocale) {
|
|
|
|
const effectiveCode = code === 'system' ? extractPrimaryLanguage(systemLocale || 'en') : code;
|
|
|
|
const effectiveCode = code === 'system' ? extractPrimaryLanguage(systemLocale || 'en') : code;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const displayNames = new Intl.DisplayNames([effectiveCode], { type: 'language' });
|
|
|
|
const displayNames = new Intl.DisplayNames([effectiveCode], { type: 'language' });
|
|
|
|
const name = displayNames.of(effectiveCode);
|
|
|
|
const name = displayNames.of(effectiveCode);
|
|
|
|
if (name) {
|
|
|
|
if (name) {
|
|
|
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
|
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
// Ignore Intl lookup failures and fall back to the raw code.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
return effectiveCode;
|
|
|
|
// Ignore Intl lookup failures and fall back to the raw code.
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return effectiveCode;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMemberLanguageInstruction(config) {
|
|
|
|
function buildMemberLanguageInstruction(config) {
|
|
|
|
const configured =
|
|
|
|
const configured =
|
|
|
|
config && typeof config.language === 'string' && config.language.trim()
|
|
|
|
config && typeof config.language === 'string' && config.language.trim() ?
|
|
|
|
? config.language.trim()
|
|
|
|
config.language.trim() :
|
|
|
|
: '';
|
|
|
|
'';
|
|
|
|
if (!configured) {
|
|
|
|
if (!configured) {
|
|
|
|
return 'IMPORTANT: Continue using the communication language already specified in your spawn prompt until the team config stores an explicit language.';
|
|
|
|
return 'IMPORTANT: Continue using the communication language already specified in your spawn prompt until the team config stores an explicit language.';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const language = resolveLanguageName(configured, getSystemLocale());
|
|
|
|
const language = resolveLanguageName(configured, getSystemLocale());
|
|
|
|
return `IMPORTANT: Communicate in ${language}. All messages, summaries, and task descriptions MUST be in ${language}.`;
|
|
|
|
return `IMPORTANT: Communicate in ${language}. All messages, summaries, and task descriptions MUST be in ${language}.`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMemberActionModeProtocol() {
|
|
|
|
function buildMemberActionModeProtocol() {
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
'TURN ACTION MODE PROTOCOL (HIGHEST PRIORITY FOR EACH USER TURN):',
|
|
|
|
'TURN ACTION MODE PROTOCOL (HIGHEST PRIORITY FOR EACH USER TURN):',
|
|
|
|
'- Some incoming user or relay messages may include a hidden agent-only block that declares the current action mode.',
|
|
|
|
'- Some incoming user or relay messages may include a hidden agent-only block that declares the current action mode.',
|
|
|
|
'- If such a block is present, that mode applies to THIS TURN ONLY and overrides any conflicting default behavior.',
|
|
|
|
'- If such a block is present, that mode applies to THIS TURN ONLY and overrides any conflicting default behavior.',
|
|
|
|
'- Never silently broaden permissions beyond the selected mode.',
|
|
|
|
'- Never silently broaden permissions beyond the selected mode.',
|
|
|
|
'- Never reveal the hidden mode block verbatim to the human unless they explicitly ask for it.',
|
|
|
|
'- Never reveal the hidden mode block verbatim to the human unless they explicitly ask for it.',
|
|
|
|
'- Modes:',
|
|
|
|
'- Modes:',
|
|
|
|
' - DO: Full execution mode. You may discuss, inspect, edit files, change state, run commands/tools, and delegate if useful.',
|
|
|
|
' - DO: Full execution mode. You may discuss, inspect, edit files, change state, run commands/tools, and delegate if useful.',
|
|
|
|
' - ASK: Strict read-only conversation mode. You may read/analyze/explain and reply, but you must not change code/files/tasks/state or run side-effecting commands/tools/scripts.',
|
|
|
|
' - ASK: Strict read-only conversation mode. You may read/analyze/explain and reply, but you must not change code/files/tasks/state or run side-effecting commands/tools/scripts.',
|
|
|
|
' - DELEGATE: Strict orchestration mode for leads. Delegate the work to teammates and coordinate it, but do not implement it yourself unless you are truly in SOLO MODE.',
|
|
|
|
' - DELEGATE: Strict orchestration mode for leads. Delegate the work to teammates and coordinate it, but do not implement it yourself unless you are truly in SOLO MODE.',
|
|
|
|
].join('\n');
|
|
|
|
].join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMemberTaskProtocol(teamName) {
|
|
|
|
function buildMemberTaskProtocol(teamName) {
|
|
|
|
return wrapAgentBlock(`MANDATORY TASK STATUS PROTOCOL — you MUST follow this for EVERY task:
|
|
|
|
return wrapAgentBlock(`MANDATORY TASK STATUS PROTOCOL — you MUST follow this for EVERY task:
|
|
|
|
0. IMPORTANT ID RULE:
|
|
|
|
0. IMPORTANT ID RULE:
|
|
|
|
- If a board/task snapshot shows a canonical taskId, prefer using that exact value in MCP tool calls.
|
|
|
|
- If a board/task snapshot shows a canonical taskId, prefer using that exact value in MCP tool calls.
|
|
|
|
- task_briefing may show short display labels like #abcd1234; MCP task tools also accept that short task ref.
|
|
|
|
- task_briefing may show short display labels like #abcd1234; MCP task tools also accept that short task ref.
|
|
|
|
@ -437,7 +434,7 @@ Failure to follow this protocol means the task board will show incorrect status.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMemberProcessProtocol(teamName) {
|
|
|
|
function buildMemberProcessProtocol(teamName) {
|
|
|
|
return wrapAgentBlock(`BACKGROUND PROCESS REGISTRATION — when you start a background process (dev server, watcher, database, etc.):
|
|
|
|
return wrapAgentBlock(`BACKGROUND PROCESS REGISTRATION — when you start a background process (dev server, watcher, database, etc.):
|
|
|
|
1. Launch with & to get PID:
|
|
|
|
1. Launch with & to get PID:
|
|
|
|
pnpm dev &
|
|
|
|
pnpm dev &
|
|
|
|
2. Register immediately with MCP tool process_register (--port and --url are optional, use when the process listens on a port):
|
|
|
|
2. Register immediately with MCP tool process_register (--port and --url are optional, use when the process listens on a port):
|
|
|
|
@ -450,7 +447,7 @@ If verification in step 3 fails or the process is missing from the list, re-regi
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMemberFormattingProtocol() {
|
|
|
|
function buildMemberFormattingProtocol() {
|
|
|
|
return wrapAgentBlock(`Hidden internal instructions rule (IMPORTANT):
|
|
|
|
return wrapAgentBlock(`Hidden internal instructions rule (IMPORTANT):
|
|
|
|
- If you send internal operational instructions to another agent/teammate that the human user must NOT see in the UI, wrap ONLY that hidden part in:
|
|
|
|
- If you send internal operational instructions to another agent/teammate that the human user must NOT see in the UI, wrap ONLY that hidden part in:
|
|
|
|
<info_for_agent>
|
|
|
|
<info_for_agent>
|
|
|
|
... hidden instructions only ...
|
|
|
|
... hidden instructions only ...
|
|
|
|
@ -460,136 +457,136 @@ function buildMemberFormattingProtocol() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeMemberName(value) {
|
|
|
|
function normalizeMemberName(value) {
|
|
|
|
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
|
|
|
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function memberBriefing(context, memberName) {
|
|
|
|
async function memberBriefing(context, memberName) {
|
|
|
|
const requestedMemberName = String(memberName).trim();
|
|
|
|
const requestedMemberName = String(memberName).trim();
|
|
|
|
const requestedMemberKey = normalizeMemberName(requestedMemberName);
|
|
|
|
const requestedMemberKey = normalizeMemberName(requestedMemberName);
|
|
|
|
const resolved = runtimeHelpers.resolveTeamMembers(context.paths);
|
|
|
|
const resolved = runtimeHelpers.resolveTeamMembers(context.paths);
|
|
|
|
const config = resolved.config || {};
|
|
|
|
const config = resolved.config || {};
|
|
|
|
if (!requestedMemberName) {
|
|
|
|
if (!requestedMemberName) {
|
|
|
|
throw new Error('Missing member name');
|
|
|
|
throw new Error('Missing member name');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (resolved.removedNames && resolved.removedNames.has(requestedMemberKey)) {
|
|
|
|
|
|
|
|
throw new Error(`Member is removed from the team: ${requestedMemberName}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const member =
|
|
|
|
|
|
|
|
resolved.members.find((entry) => normalizeMemberName(entry && entry.name) === requestedMemberKey) ||
|
|
|
|
|
|
|
|
null;
|
|
|
|
|
|
|
|
if (!member) {
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
|
|
`Member not found in team metadata or inboxes: ${requestedMemberName}`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
|
|
|
|
const effectiveMember = member;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const role =
|
|
|
|
|
|
|
|
typeof effectiveMember.role === 'string' && effectiveMember.role.trim()
|
|
|
|
|
|
|
|
? effectiveMember.role.trim()
|
|
|
|
|
|
|
|
: typeof effectiveMember.agentType === 'string' && effectiveMember.agentType.trim()
|
|
|
|
|
|
|
|
? effectiveMember.agentType.trim()
|
|
|
|
|
|
|
|
: 'team member';
|
|
|
|
|
|
|
|
const workflow =
|
|
|
|
|
|
|
|
typeof effectiveMember.workflow === 'string' && effectiveMember.workflow.trim()
|
|
|
|
|
|
|
|
? effectiveMember.workflow.trim()
|
|
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
const cwd =
|
|
|
|
|
|
|
|
typeof effectiveMember.cwd === 'string' && effectiveMember.cwd.trim()
|
|
|
|
|
|
|
|
? effectiveMember.cwd.trim()
|
|
|
|
|
|
|
|
: typeof config.projectPath === 'string' && config.projectPath.trim()
|
|
|
|
|
|
|
|
? config.projectPath.trim()
|
|
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const activeProcesses = processStore
|
|
|
|
|
|
|
|
.listProcesses(context.paths)
|
|
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
|
|
(entry) =>
|
|
|
|
|
|
|
|
entry &&
|
|
|
|
|
|
|
|
entry.alive &&
|
|
|
|
|
|
|
|
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const taskQueue = await taskBriefing(context, requestedMemberName);
|
|
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
|
|
`Member briefing for ${requestedMemberName} on team "${context.teamName}" (${context.teamName}).`,
|
|
|
|
|
|
|
|
`Role: ${role}.`,
|
|
|
|
|
|
|
|
`CRITICAL: If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on that same task, FIRST leave a short task comment saying what you are about to do, THEN move it to in_progress with task_start, THEN do the work, and when finished leave a short result comment and move it to done with task_complete. Never skip this comment -> reopen -> work -> comment -> done cycle.`,
|
|
|
|
|
|
|
|
`CRITICAL: A newly assigned task must NOT remain silently pending/TODO. If you are idle and the task is ready to start, start it now. If it must wait because you are already finishing another task, blocked, or still need more context, leave a short task comment on the waiting task immediately with the reason and your best ETA or what you are waiting on, keep it in pending/TODO, and only move it to in_progress with task_start when you truly begin.`,
|
|
|
|
|
|
|
|
`Team lead: ${leadName}.`,
|
|
|
|
|
|
|
|
buildMemberLanguageInstruction(config),
|
|
|
|
|
|
|
|
`You must NOT start work, claim tasks, or improvise task/process protocol before reading and following this briefing.`,
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (workflow) {
|
|
|
|
|
|
|
|
lines.push('', 'Workflow:', workflow);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (cwd) {
|
|
|
|
|
|
|
|
lines.push('', `Working directory: ${cwd}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
`Bootstrap flow:`,
|
|
|
|
|
|
|
|
`1. Use this briefing as your durable rules source.`,
|
|
|
|
|
|
|
|
`2. Use task_briefing as your compact queue view whenever you need to see assigned work.`,
|
|
|
|
|
|
|
|
`3. Before starting a pending or needs-fix task, call task_get for that specific task if you need the full context. A newly assigned task must not remain silently pending/TODO: if you are idle and the task is ready to start, start it now; if it must wait because another task is already active, because it is blocked, or because you still need more context, add a short task comment with the reason + ETA or what you are waiting on and keep it pending/TODO until you actually begin.`,
|
|
|
|
|
|
|
|
`4. If this briefing was requested during reconnect, resume in_progress work first, then needs-fix tasks, then pending tasks.`,
|
|
|
|
|
|
|
|
`5. If you cannot obtain the context you need, notify your team lead ("${leadName}") and wait instead of guessing.`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberActionModeProtocol(),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberFormattingProtocol(),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberTaskProtocol(context.teamName),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberProcessProtocol(context.teamName)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (activeProcesses.length > 0) {
|
|
|
|
|
|
|
|
lines.push('', 'Active registered processes owned by you:');
|
|
|
|
|
|
|
|
for (const entry of activeProcesses) {
|
|
|
|
|
|
|
|
const bits = [`- ${entry.label} (pid ${entry.pid})`];
|
|
|
|
|
|
|
|
if (entry.port != null) bits.push(`port ${entry.port}`);
|
|
|
|
|
|
|
|
if (entry.url) bits.push(`url ${entry.url}`);
|
|
|
|
|
|
|
|
if (entry.command) bits.push(`command ${entry.command}`);
|
|
|
|
|
|
|
|
lines.push(bits.join(', '));
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (resolved.removedNames && resolved.removedNames.has(requestedMemberKey)) {
|
|
|
|
|
|
|
|
throw new Error(`Member is removed from the team: ${requestedMemberName}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const member =
|
|
|
|
|
|
|
|
resolved.members.find((entry) => normalizeMemberName(entry && entry.name) === requestedMemberKey) ||
|
|
|
|
|
|
|
|
null;
|
|
|
|
|
|
|
|
if (!member) {
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
|
|
`Member not found in team metadata or inboxes: ${requestedMemberName}`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const leadName = runtimeHelpers.inferLeadName(context.paths);
|
|
|
|
|
|
|
|
const effectiveMember = member;
|
|
|
|
|
|
|
|
|
|
|
|
lines.push('', taskQueue);
|
|
|
|
const role =
|
|
|
|
return lines.join('\n');
|
|
|
|
typeof effectiveMember.role === 'string' && effectiveMember.role.trim() ?
|
|
|
|
|
|
|
|
effectiveMember.role.trim() :
|
|
|
|
|
|
|
|
typeof effectiveMember.agentType === 'string' && effectiveMember.agentType.trim() ?
|
|
|
|
|
|
|
|
effectiveMember.agentType.trim() :
|
|
|
|
|
|
|
|
'team member';
|
|
|
|
|
|
|
|
const workflow =
|
|
|
|
|
|
|
|
typeof effectiveMember.workflow === 'string' && effectiveMember.workflow.trim() ?
|
|
|
|
|
|
|
|
effectiveMember.workflow.trim() :
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
const cwd =
|
|
|
|
|
|
|
|
typeof effectiveMember.cwd === 'string' && effectiveMember.cwd.trim() ?
|
|
|
|
|
|
|
|
effectiveMember.cwd.trim() :
|
|
|
|
|
|
|
|
typeof config.projectPath === 'string' && config.projectPath.trim() ?
|
|
|
|
|
|
|
|
config.projectPath.trim() :
|
|
|
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const activeProcesses = processStore
|
|
|
|
|
|
|
|
.listProcesses(context.paths)
|
|
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
|
|
(entry) =>
|
|
|
|
|
|
|
|
entry &&
|
|
|
|
|
|
|
|
entry.alive &&
|
|
|
|
|
|
|
|
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const taskQueue = await taskBriefing(context, requestedMemberName);
|
|
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
|
|
`Member briefing for ${requestedMemberName} on team "${context.teamName}" (${context.teamName}).`,
|
|
|
|
|
|
|
|
`Role: ${role}.`,
|
|
|
|
|
|
|
|
`CRITICAL: If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on that same task, FIRST leave a short task comment saying what you are about to do, THEN move it to in_progress with task_start, THEN do the work, and when finished leave a short result comment and move it to done with task_complete. Never skip this comment -> reopen -> work -> comment -> done cycle.`,
|
|
|
|
|
|
|
|
`CRITICAL: A newly assigned task must NOT remain silently pending/TODO. If you are idle and the task is ready to start, start it now. If it must wait because you are already finishing another task, blocked, or still need more context, leave a short task comment on the waiting task immediately with the reason and your best ETA or what you are waiting on, keep it in pending/TODO, and only move it to in_progress with task_start when you truly begin.`,
|
|
|
|
|
|
|
|
`Team lead: ${leadName}.`,
|
|
|
|
|
|
|
|
buildMemberLanguageInstruction(config),
|
|
|
|
|
|
|
|
`You must NOT start work, claim tasks, or improvise task/process protocol before reading and following this briefing.`,
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (workflow) {
|
|
|
|
|
|
|
|
lines.push('', 'Workflow:', workflow);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (cwd) {
|
|
|
|
|
|
|
|
lines.push('', `Working directory: ${cwd}`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
`Bootstrap flow:`,
|
|
|
|
|
|
|
|
`1. Use this briefing as your durable rules source.`,
|
|
|
|
|
|
|
|
`2. Use task_briefing as your compact queue view whenever you need to see assigned work.`,
|
|
|
|
|
|
|
|
`3. Before starting a pending or needs-fix task, call task_get for that specific task if you need the full context. A newly assigned task must not remain silently pending/TODO: if you are idle and the task is ready to start, start it now; if it must wait because another task is already active, because it is blocked, or because you still need more context, add a short task comment with the reason + ETA or what you are waiting on and keep it pending/TODO until you actually begin.`,
|
|
|
|
|
|
|
|
`4. If this briefing was requested during reconnect, resume in_progress work first, then needs-fix tasks, then pending tasks.`,
|
|
|
|
|
|
|
|
`5. If you cannot obtain the context you need, notify your team lead ("${leadName}") and wait instead of guessing.`
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberActionModeProtocol(),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberFormattingProtocol(),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberTaskProtocol(context.teamName),
|
|
|
|
|
|
|
|
'',
|
|
|
|
|
|
|
|
buildMemberProcessProtocol(context.teamName)
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (activeProcesses.length > 0) {
|
|
|
|
|
|
|
|
lines.push('', 'Active registered processes owned by you:');
|
|
|
|
|
|
|
|
for (const entry of activeProcesses) {
|
|
|
|
|
|
|
|
const bits = [`- ${entry.label} (pid ${entry.pid})`];
|
|
|
|
|
|
|
|
if (entry.port != null) bits.push(`port ${entry.port}`);
|
|
|
|
|
|
|
|
if (entry.url) bits.push(`url ${entry.url}`);
|
|
|
|
|
|
|
|
if (entry.command) bits.push(`command ${entry.command}`);
|
|
|
|
|
|
|
|
lines.push(bits.join(', '));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lines.push('', taskQueue);
|
|
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
module.exports = {
|
|
|
|
addTaskAttachmentMeta,
|
|
|
|
addTaskAttachmentMeta,
|
|
|
|
addTaskComment,
|
|
|
|
addTaskComment,
|
|
|
|
appendHistoryEvent: taskStore.appendHistoryEvent,
|
|
|
|
appendHistoryEvent: taskStore.appendHistoryEvent,
|
|
|
|
attachTaskFile,
|
|
|
|
attachTaskFile,
|
|
|
|
attachCommentFile,
|
|
|
|
attachCommentFile,
|
|
|
|
completeTask,
|
|
|
|
completeTask,
|
|
|
|
createTask,
|
|
|
|
createTask,
|
|
|
|
getTask,
|
|
|
|
getTask,
|
|
|
|
linkTask,
|
|
|
|
linkTask,
|
|
|
|
listDeletedTasks,
|
|
|
|
listDeletedTasks,
|
|
|
|
listTasks,
|
|
|
|
listTasks,
|
|
|
|
removeTaskAttachment,
|
|
|
|
removeTaskAttachment,
|
|
|
|
resolveTaskId,
|
|
|
|
resolveTaskId,
|
|
|
|
restoreTask,
|
|
|
|
restoreTask,
|
|
|
|
setNeedsClarification,
|
|
|
|
setNeedsClarification,
|
|
|
|
setTaskOwner,
|
|
|
|
setTaskOwner,
|
|
|
|
setTaskStatus,
|
|
|
|
setTaskStatus,
|
|
|
|
softDeleteTask,
|
|
|
|
softDeleteTask,
|
|
|
|
startTask,
|
|
|
|
startTask,
|
|
|
|
memberBriefing,
|
|
|
|
memberBriefing,
|
|
|
|
taskBriefing,
|
|
|
|
taskBriefing,
|
|
|
|
unlinkTask,
|
|
|
|
unlinkTask,
|
|
|
|
updateTask: (context, taskRef, updater) =>
|
|
|
|
updateTask: (context, taskRef, updater) =>
|
|
|
|
taskStore.updateTask(context.paths, taskRef, updater),
|
|
|
|
taskStore.updateTask(context.paths, taskRef, updater),
|
|
|
|
updateTaskFields,
|
|
|
|
updateTaskFields,
|
|
|
|
};
|
|
|
|
};
|