test: expand claude work sync live scenarios
This commit is contained in:
parent
7cb6cddee8
commit
bfd8b30ad5
1 changed files with 291 additions and 204 deletions
|
|
@ -49,6 +49,26 @@ const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_
|
|||
const DEFAULT_MODEL = 'sonnet';
|
||||
const DEFAULT_EFFORT = 'low' as const;
|
||||
|
||||
type ClaudeStopHookLiveScenarioState = 'still_working' | 'caught_up';
|
||||
|
||||
interface ClaudeStopHookLiveScenarioContext {
|
||||
marker: string;
|
||||
memberName: string;
|
||||
teamName: string;
|
||||
controlUrl: string;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
interface ClaudeStopHookLiveScenario {
|
||||
markerSuffix: string;
|
||||
subjectPrefix: string;
|
||||
expectedState: ClaudeStopHookLiveScenarioState;
|
||||
expectedTaskStatus: 'in_progress' | 'completed';
|
||||
expectedMarkerText(marker: string): string;
|
||||
buildTaskPromptLines(context: ClaudeStopHookLiveScenarioContext): string[];
|
||||
buildInstructionLines(context: Required<ClaudeStopHookLiveScenarioContext>): string[];
|
||||
}
|
||||
|
||||
liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
||||
let tempDir: string;
|
||||
let tempClaudeRoot: string;
|
||||
|
|
@ -124,226 +144,293 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it(
|
||||
'launches a real Claude teammate, accepts its work-sync report, and ingests its Stop hook event',
|
||||
async () => {
|
||||
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||
expect(orchestratorCli).toBeTruthy();
|
||||
await assertExecutable(orchestratorCli!);
|
||||
async function runClaudeStopHookLiveScenario(
|
||||
scenario: ClaudeStopHookLiveScenario
|
||||
): Promise<void> {
|
||||
const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||
expect(orchestratorCli).toBeTruthy();
|
||||
await assertExecutable(orchestratorCli!);
|
||||
|
||||
const model = process.env.MEMBER_WORK_SYNC_CLAUDE_MODEL?.trim() || DEFAULT_MODEL;
|
||||
const marker = `member-work-sync-claude-stop-live-${Date.now()}`;
|
||||
const memberName = 'alice';
|
||||
teamName = `member-work-sync-claude-stop-${Date.now()}`;
|
||||
const projectPath = path.join(tempDir, 'project');
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(projectPath, 'README.md'),
|
||||
'# Member work sync Claude Stop hook live e2e\n\nKeep this project intentionally tiny.\n',
|
||||
'utf8'
|
||||
);
|
||||
const model = process.env.MEMBER_WORK_SYNC_CLAUDE_MODEL?.trim() || DEFAULT_MODEL;
|
||||
const startedAt = Date.now();
|
||||
const marker = `member-work-sync-claude-stop-live-${scenario.markerSuffix}-${startedAt}`;
|
||||
const memberName = 'alice';
|
||||
teamName = `member-work-sync-claude-stop-${scenario.markerSuffix}-${startedAt}`;
|
||||
const projectPath = path.join(tempDir, 'project');
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(projectPath, 'README.md'),
|
||||
'# Member work sync Claude Stop hook live e2e\n\nKeep this project intentionally tiny.\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
svc = new TeamProvisioningService();
|
||||
const activeService = svc;
|
||||
const teamDataService = new TeamDataService();
|
||||
const configReader = new TeamConfigReader();
|
||||
const membersMetaStore = new TeamMembersMetaStore();
|
||||
feature = createMemberWorkSyncFeature({
|
||||
teamsBasePath: getTeamsBasePath(),
|
||||
configReader,
|
||||
taskReader: new TeamTaskReader(),
|
||||
kanbanManager: new TeamKanbanManager(),
|
||||
membersMetaStore,
|
||||
isTeamActive: (name) =>
|
||||
activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
|
||||
listLifecycleActiveTeamNames: async () => [teamName!],
|
||||
nudgeSideEffectsEnabled: false,
|
||||
queueQuietWindowMs: 500,
|
||||
// Native Claude teammates are registered by the real lead process, but in this
|
||||
// headless harness their bootstrap turn can finish before there is a durable
|
||||
// member process to prompt. The live assertion below still uses a real Claude
|
||||
// process, real MCP calls, and a real Stop hook payload; this seam keeps the
|
||||
// test focused on hook ingestion instead of tmux liveness.
|
||||
runtimeTurnSettledTargetResolver: {
|
||||
resolve: async (event) => {
|
||||
if (event.provider !== 'claude') {
|
||||
return { ok: false, reason: 'unsupported_provider' };
|
||||
}
|
||||
if (!teamName) {
|
||||
return { ok: false, reason: 'missing_team' };
|
||||
}
|
||||
const config = await configReader.getConfig(teamName);
|
||||
const leadSessionId = config?.leadSessionId?.trim();
|
||||
if (!leadSessionId || event.sessionId !== leadSessionId) {
|
||||
return { ok: false, reason: 'no_matching_member_session' };
|
||||
}
|
||||
return { ok: true, teamName, memberName };
|
||||
},
|
||||
svc = new TeamProvisioningService();
|
||||
const activeService = svc;
|
||||
const teamDataService = new TeamDataService();
|
||||
const configReader = new TeamConfigReader();
|
||||
const membersMetaStore = new TeamMembersMetaStore();
|
||||
feature = createMemberWorkSyncFeature({
|
||||
teamsBasePath: getTeamsBasePath(),
|
||||
configReader,
|
||||
taskReader: new TeamTaskReader(),
|
||||
kanbanManager: new TeamKanbanManager(),
|
||||
membersMetaStore,
|
||||
isTeamActive: (name) =>
|
||||
activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
|
||||
listLifecycleActiveTeamNames: async () => [teamName!],
|
||||
nudgeSideEffectsEnabled: false,
|
||||
queueQuietWindowMs: 500,
|
||||
// Native Claude teammates are registered by the real lead process, but in this
|
||||
// headless harness their bootstrap turn can finish before there is a durable
|
||||
// member process to prompt. These live assertions still use a real Claude
|
||||
// process, real MCP calls, and real Stop hook payloads; this seam keeps the
|
||||
// tests focused on hook ingestion instead of tmux liveness.
|
||||
runtimeTurnSettledTargetResolver: {
|
||||
resolve: async (event) => {
|
||||
if (event.provider !== 'claude') {
|
||||
return { ok: false, reason: 'unsupported_provider' };
|
||||
}
|
||||
if (!teamName) {
|
||||
return { ok: false, reason: 'missing_team' };
|
||||
}
|
||||
const config = await configReader.getConfig(teamName);
|
||||
const leadSessionId = config?.leadSessionId?.trim();
|
||||
if (!leadSessionId || event.sessionId !== leadSessionId) {
|
||||
return { ok: false, reason: 'no_matching_member_session' };
|
||||
}
|
||||
return { ok: true, teamName, memberName };
|
||||
},
|
||||
});
|
||||
activeService.setTeamChangeEmitter((event: TeamChangeEvent) =>
|
||||
feature!.noteTeamChange(event)
|
||||
);
|
||||
activeService.setRuntimeTurnSettledHookSettingsProvider((input) =>
|
||||
feature!.buildRuntimeTurnSettledHookSettings(input)
|
||||
);
|
||||
},
|
||||
});
|
||||
activeService.setTeamChangeEmitter((event: TeamChangeEvent) => feature!.noteTeamChange(event));
|
||||
activeService.setRuntimeTurnSettledHookSettingsProvider((input) =>
|
||||
feature!.buildRuntimeTurnSettledHookSettings(input)
|
||||
);
|
||||
|
||||
controlServer = await startMemberWorkSyncControlServer(feature);
|
||||
process.env.CLAUDE_TEAM_CONTROL_URL = controlServer.baseUrl;
|
||||
activeService.setControlApiBaseUrlResolver(async () => controlServer?.baseUrl ?? null);
|
||||
await fs.writeFile(
|
||||
path.join(tempClaudeRoot, 'team-control-api.json'),
|
||||
JSON.stringify({ baseUrl: controlServer.baseUrl }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
controlServer = await startMemberWorkSyncControlServer(feature);
|
||||
process.env.CLAUDE_TEAM_CONTROL_URL = controlServer.baseUrl;
|
||||
activeService.setControlApiBaseUrlResolver(async () => controlServer?.baseUrl ?? null);
|
||||
await fs.writeFile(
|
||||
path.join(tempClaudeRoot, 'team-control-api.json'),
|
||||
JSON.stringify({ baseUrl: controlServer.baseUrl }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const progressEvents: TeamProvisioningProgress[] = [];
|
||||
await activeService.createTeam(
|
||||
{
|
||||
teamName,
|
||||
cwd: projectPath,
|
||||
providerId: 'anthropic',
|
||||
model,
|
||||
effort: DEFAULT_EFFORT,
|
||||
skipPermissions: true,
|
||||
prompt: [
|
||||
'Keep launch work minimal and wait for the explicit live-test instruction.',
|
||||
'Do not inspect tasks or send messages until the next user turn.',
|
||||
].join(' '),
|
||||
members: [
|
||||
{
|
||||
name: memberName,
|
||||
role: 'Developer',
|
||||
providerId: 'anthropic',
|
||||
model,
|
||||
effort: DEFAULT_EFFORT,
|
||||
},
|
||||
],
|
||||
},
|
||||
(progress) => {
|
||||
progressEvents.push(progress);
|
||||
}
|
||||
);
|
||||
|
||||
await waitUntil(async () => {
|
||||
const last = progressEvents.at(-1);
|
||||
if (last?.state === 'failed') {
|
||||
throw new Error(formatProgressDump(progressEvents));
|
||||
}
|
||||
return last?.state === 'ready';
|
||||
}, 240_000);
|
||||
|
||||
await expect(
|
||||
fs.stat(
|
||||
path.join(
|
||||
getTeamsBasePath(),
|
||||
'.member-work-sync/runtime-hooks/bin/turn-settled-hook-v1.sh'
|
||||
)
|
||||
)
|
||||
).resolves.toMatchObject({ mode: expect.any(Number) });
|
||||
|
||||
const task = await teamDataService.createTask(teamName, {
|
||||
subject: `Member work sync Claude Stop hook live lease ${marker}`,
|
||||
owner: memberName,
|
||||
startImmediately: true,
|
||||
const progressEvents: TeamProvisioningProgress[] = [];
|
||||
await activeService.createTeam(
|
||||
{
|
||||
teamName,
|
||||
cwd: projectPath,
|
||||
providerId: 'anthropic',
|
||||
model,
|
||||
effort: DEFAULT_EFFORT,
|
||||
skipPermissions: true,
|
||||
prompt: [
|
||||
'Keep launch work minimal and wait for the explicit live-test instruction.',
|
||||
'Do not inspect tasks or send messages until the next user turn.',
|
||||
].join(' '),
|
||||
members: [
|
||||
{
|
||||
name: memberName,
|
||||
role: 'Developer',
|
||||
providerId: 'anthropic',
|
||||
model,
|
||||
effort: DEFAULT_EFFORT,
|
||||
},
|
||||
],
|
||||
},
|
||||
(progress) => {
|
||||
progressEvents.push(progress);
|
||||
}
|
||||
);
|
||||
|
||||
await waitUntil(async () => {
|
||||
const last = progressEvents.at(-1);
|
||||
if (last?.state === 'failed') {
|
||||
throw new Error(formatProgressDump(progressEvents));
|
||||
}
|
||||
return last?.state === 'ready';
|
||||
}, 240_000);
|
||||
|
||||
await expect(
|
||||
fs.stat(
|
||||
path.join(
|
||||
getTeamsBasePath(),
|
||||
'.member-work-sync/runtime-hooks/bin/turn-settled-hook-v1.sh'
|
||||
)
|
||||
)
|
||||
).resolves.toMatchObject({ mode: expect.any(Number) });
|
||||
|
||||
const taskPromptContext: ClaudeStopHookLiveScenarioContext = {
|
||||
marker,
|
||||
memberName,
|
||||
teamName,
|
||||
controlUrl: controlServer.baseUrl,
|
||||
};
|
||||
const task = await teamDataService.createTask(teamName, {
|
||||
subject: `${scenario.subjectPrefix} ${marker}`,
|
||||
owner: memberName,
|
||||
startImmediately: true,
|
||||
prompt: scenario.buildTaskPromptLines(taskPromptContext).join('\n'),
|
||||
});
|
||||
feature.noteTeamChange({ type: 'task', teamName, taskId: task.id });
|
||||
|
||||
await waitUntil(async () => {
|
||||
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||
return (
|
||||
status.memberName === memberName &&
|
||||
status.providerId === 'anthropic' &&
|
||||
status.agenda.items.some((item) => item.taskId === task.id) &&
|
||||
status.shadow?.wouldNudge === true
|
||||
);
|
||||
}, 30_000);
|
||||
|
||||
await activeService.sendMessageToTeam(
|
||||
teamName,
|
||||
scenario
|
||||
.buildInstructionLines({
|
||||
...taskPromptContext,
|
||||
taskId: task.id,
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
await waitUntil(async () => {
|
||||
await feature!.replayPendingReports([teamName!]);
|
||||
const [status, tasks] = await Promise.all([
|
||||
feature!.getStatus({ teamName: teamName!, memberName }),
|
||||
new TeamTaskReader().getTasks(teamName!),
|
||||
]);
|
||||
const currentTask = tasks.find((candidate) => candidate.id === task.id);
|
||||
const expectedMarker = scenario.expectedMarkerText(marker);
|
||||
const hasMarkerComment = currentTask?.comments?.some(
|
||||
(comment) => comment.author === memberName && comment.text.includes(expectedMarker)
|
||||
);
|
||||
return Boolean(
|
||||
hasMarkerComment &&
|
||||
currentTask?.status === scenario.expectedTaskStatus &&
|
||||
status.report?.accepted &&
|
||||
status.report.state === scenario.expectedState
|
||||
);
|
||||
}, 300_000, 2_000, async () =>
|
||||
formatMemberWorkSyncDiagnostics({
|
||||
feature: feature!,
|
||||
teamName: teamName!,
|
||||
memberName,
|
||||
taskId: task.id,
|
||||
})
|
||||
);
|
||||
|
||||
const beforeTurnSettledReconciled = feature.getQueueDiagnostics().reconciled;
|
||||
await waitUntil(async () => {
|
||||
await feature!.drainRuntimeTurnSettledEvents();
|
||||
const metas = await readRuntimeTurnSettledProcessedMetas(getTeamsBasePath());
|
||||
return metas.some(({ meta }) => {
|
||||
const event = meta.event as Record<string, unknown> | undefined;
|
||||
return (
|
||||
meta.outcome === 'enqueued' &&
|
||||
meta.teamName === teamName &&
|
||||
meta.memberName === memberName &&
|
||||
event?.provider === 'claude'
|
||||
);
|
||||
});
|
||||
}, 180_000, 2_000, async () =>
|
||||
formatMemberWorkSyncDiagnostics({
|
||||
feature: feature!,
|
||||
teamName: teamName!,
|
||||
memberName,
|
||||
taskId: task.id,
|
||||
})
|
||||
);
|
||||
|
||||
await waitUntil(
|
||||
async () => feature!.getQueueDiagnostics().reconciled > beforeTurnSettledReconciled,
|
||||
30_000,
|
||||
500
|
||||
);
|
||||
|
||||
const [finalStatus, metrics] = await Promise.all([
|
||||
feature.getStatus({ teamName, memberName }),
|
||||
feature.getMetrics({ teamName }),
|
||||
]);
|
||||
expect(finalStatus.state).toBe(scenario.expectedState);
|
||||
expect(finalStatus.report).toMatchObject({
|
||||
accepted: true,
|
||||
state: scenario.expectedState,
|
||||
});
|
||||
if (scenario.expectedState === 'caught_up') {
|
||||
expect(finalStatus.agenda.items).toHaveLength(0);
|
||||
} else {
|
||||
expect(finalStatus.agenda.items.some((item) => item.taskId === task.id)).toBe(true);
|
||||
}
|
||||
expect(metrics.recentEvents.some((event) => event.kind === 'report_accepted')).toBe(true);
|
||||
await expect(feature.dispatchDueNudges([teamName])).resolves.toMatchObject({
|
||||
claimed: 0,
|
||||
delivered: 0,
|
||||
});
|
||||
}
|
||||
|
||||
it(
|
||||
'launches a real Claude teammate, accepts a still-working report, and ingests its Stop hook event',
|
||||
async () =>
|
||||
runClaudeStopHookLiveScenario({
|
||||
markerSuffix: 'lease',
|
||||
subjectPrefix: 'Member work sync Claude Stop hook live lease',
|
||||
expectedState: 'still_working',
|
||||
expectedTaskStatus: 'in_progress',
|
||||
expectedMarkerText: (marker) => `${marker}:still-working`,
|
||||
buildTaskPromptLines: ({ marker, memberName, teamName, controlUrl }) => [
|
||||
`This is a live member-work-sync validation task. Marker: ${marker}.`,
|
||||
'Do not edit files and do not complete this task.',
|
||||
'Call task_start for this task.',
|
||||
`Add one task comment containing exactly: ${marker}:still-working.`,
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlServer.baseUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlServer.baseUrl}", state "still_working", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and the current task id if available.`,
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlUrl}", state "still_working", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and the current task id if available.`,
|
||||
'After that stop. Do not send a user-visible message.',
|
||||
].join('\n'),
|
||||
});
|
||||
feature.noteTeamChange({ type: 'task', teamName, taskId: task.id });
|
||||
await activeService.sendMessageToTeam(
|
||||
teamName,
|
||||
[
|
||||
],
|
||||
buildInstructionLines: ({ marker, memberName, teamName, controlUrl, taskId }) => [
|
||||
`Live member-work-sync validation instruction. Marker: ${marker}.`,
|
||||
`Use the board MCP tools as member "${memberName}" for this validation.`,
|
||||
`Call task_get for taskId "${task.id}", then task_start.`,
|
||||
`Call task_get for taskId "${taskId}", then task_start.`,
|
||||
`Add one task comment containing exactly: ${marker}:still-working.`,
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlServer.baseUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlServer.baseUrl}", state "still_working", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and taskIds ["${task.id}"].`,
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlUrl}", state "still_working", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and taskIds ["${taskId}"].`,
|
||||
'After that stop. Do not complete the task. Do not send a user-visible message.',
|
||||
].join('\n')
|
||||
);
|
||||
],
|
||||
}),
|
||||
420_000
|
||||
);
|
||||
|
||||
await waitUntil(async () => {
|
||||
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||
return (
|
||||
status.memberName === memberName &&
|
||||
status.providerId === 'anthropic' &&
|
||||
status.agenda.items.some((item) => item.taskId === task.id) &&
|
||||
status.shadow?.wouldNudge === true
|
||||
);
|
||||
}, 30_000);
|
||||
|
||||
await waitUntil(async () => {
|
||||
await feature!.replayPendingReports([teamName!]);
|
||||
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||
if (status.report?.accepted && status.report.state === 'still_working') {
|
||||
return true;
|
||||
}
|
||||
const tasks = await new TeamTaskReader().getTasks(teamName!);
|
||||
const currentTask = tasks.find((candidate) => candidate.id === task.id);
|
||||
const hasMarkerComment = currentTask?.comments?.some((comment) =>
|
||||
comment.text.includes(`${marker}:still-working`)
|
||||
);
|
||||
return Boolean(hasMarkerComment && status.report?.accepted);
|
||||
}, 300_000, 2_000, async () =>
|
||||
formatMemberWorkSyncDiagnostics({
|
||||
feature: feature!,
|
||||
teamName: teamName!,
|
||||
memberName,
|
||||
taskId: task.id,
|
||||
})
|
||||
);
|
||||
|
||||
const beforeTurnSettledReconciled = feature.getQueueDiagnostics().reconciled;
|
||||
await waitUntil(async () => {
|
||||
await feature!.drainRuntimeTurnSettledEvents();
|
||||
const metas = await readRuntimeTurnSettledProcessedMetas(getTeamsBasePath());
|
||||
return metas.some(({ meta }) => {
|
||||
const event = meta.event as Record<string, unknown> | undefined;
|
||||
return (
|
||||
meta.outcome === 'enqueued' &&
|
||||
meta.teamName === teamName &&
|
||||
meta.memberName === memberName &&
|
||||
event?.provider === 'claude'
|
||||
);
|
||||
});
|
||||
}, 180_000, 2_000, async () =>
|
||||
formatMemberWorkSyncDiagnostics({
|
||||
feature: feature!,
|
||||
teamName: teamName!,
|
||||
memberName,
|
||||
taskId: task.id,
|
||||
})
|
||||
);
|
||||
|
||||
await waitUntil(
|
||||
async () => feature!.getQueueDiagnostics().reconciled > beforeTurnSettledReconciled,
|
||||
30_000,
|
||||
500
|
||||
);
|
||||
|
||||
const [finalStatus, metrics] = await Promise.all([
|
||||
feature.getStatus({ teamName, memberName }),
|
||||
feature.getMetrics({ teamName }),
|
||||
]);
|
||||
expect(finalStatus.state).toBe('still_working');
|
||||
expect(finalStatus.report).toMatchObject({
|
||||
accepted: true,
|
||||
state: 'still_working',
|
||||
});
|
||||
expect(metrics.recentEvents.some((event) => event.kind === 'report_accepted')).toBe(true);
|
||||
await expect(feature.dispatchDueNudges([teamName])).resolves.toMatchObject({
|
||||
claimed: 0,
|
||||
delivered: 0,
|
||||
});
|
||||
},
|
||||
it(
|
||||
'launches a real Claude teammate, completes work, reports caught-up, and ingests its Stop hook event',
|
||||
async () =>
|
||||
runClaudeStopHookLiveScenario({
|
||||
markerSuffix: 'caught-up',
|
||||
subjectPrefix: 'Member work sync Claude Stop hook live caught-up',
|
||||
expectedState: 'caught_up',
|
||||
expectedTaskStatus: 'completed',
|
||||
expectedMarkerText: (marker) => `${marker}:completed`,
|
||||
buildTaskPromptLines: ({ marker, memberName, teamName, controlUrl }) => [
|
||||
`This is a live member-work-sync caught-up validation task. Marker: ${marker}.`,
|
||||
'Do not edit files.',
|
||||
'Call task_start for this task.',
|
||||
`Add one task comment containing exactly: ${marker}:completed.`,
|
||||
'Then call task_complete for this task.',
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlUrl}", state "caught_up", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and no taskIds.`,
|
||||
'After that stop. Do not send a user-visible message.',
|
||||
],
|
||||
buildInstructionLines: ({ marker, memberName, teamName, controlUrl, taskId }) => [
|
||||
`Live member-work-sync caught-up validation instruction. Marker: ${marker}.`,
|
||||
`Use the board MCP tools as member "${memberName}" for this validation.`,
|
||||
`Call task_get for taskId "${taskId}", then task_start.`,
|
||||
`Add one task comment containing exactly: ${marker}:completed.`,
|
||||
`Call task_complete for taskId "${taskId}".`,
|
||||
`Then call member_work_sync_status with teamName "${teamName}", memberName "${memberName}", and controlUrl "${controlUrl}".`,
|
||||
`Then call member_work_sync_report with teamName "${teamName}", memberName "${memberName}", controlUrl "${controlUrl}", state "caught_up", the exact agendaFingerprint and reportToken returned by member_work_sync_status, and no taskIds.`,
|
||||
'After that stop. Do not send a user-visible message.',
|
||||
],
|
||||
}),
|
||||
420_000
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue