feat: add high-risk paired plan gate

This commit is contained in:
Eyejoker
2026-03-29 02:12:28 +09:00
parent c76ed1d10a
commit b536c90716
9 changed files with 1288 additions and 20 deletions

View File

@@ -12,9 +12,26 @@ const SESSION_COMMAND_CONTROL_PATTERNS = [
/^Conversation compacted\.$/,
/^Review snapshot updated\.(?:\n|$)/,
/^Review request recorded, but the owner workspace is not ready yet\.(?:\n|$)/,
/^Plan review is required before formal review for this high-risk task\.(?:\n|$)/,
/^Task risk updated\.(?:\n|$)/,
/^Plan recorded\.(?:\n|$)/,
/^Plan approved\.(?:\n|$)/,
/^Plan changes requested\.(?:\n|$)/,
/^Risk updates must be handled by the owner service\.$/,
/^Plan recording must be handled by the owner service\.$/,
/^Plan approval must be handled by the reviewer service\.$/,
/^Plan review commands are only required for high-risk tasks\.$/,
/^Plan artifacts are incomplete\.(?:\n|$)/,
/^Review is unavailable for this room\./,
];
function normalizeSessionCommandText(
content: string,
triggerPattern: RegExp,
): string {
return content.trim().replace(triggerPattern, '').trim();
}
/**
* Extract a session slash command from a message, stripping the trigger prefix if present.
* Returns the slash command (e.g., '/compact') or null if not a session command.
@@ -23,11 +40,16 @@ export function extractSessionCommand(
content: string,
triggerPattern: RegExp,
): string | null {
let text = content.trim();
text = text.replace(triggerPattern, '').trim();
const text = normalizeSessionCommandText(content, triggerPattern);
if (text === '/compact') return '/compact';
if (text === '/clear') return '/clear';
if (text === '/review' || text === '/review-ready') return '/review';
if (/^\/risk(?:\s|$)/.test(text)) return '/risk';
if (/^\/plan(?:\s|$)/.test(text)) return '/plan';
if (text === '/approve-plan') return '/approve-plan';
if (/^\/request-plan-changes(?:\s|$)/.test(text)) {
return '/request-plan-changes';
}
return null;
}
@@ -73,6 +95,14 @@ export interface SessionCommandDeps {
/** Whether the denied sender would normally be allowed to interact (for denial messages). */
canSenderInteract: (msg: NewMessage) => boolean;
markReviewReady: () => Promise<string | null>;
setTaskRiskLevel: (riskLevel: 'low' | 'high') => Promise<string | null>;
recordPlan: (plan: {
planBrief: string;
acceptanceCriteria: string;
riskSummary: string;
}) => Promise<string | null>;
approvePlan: () => Promise<string | null>;
requestPlanChanges: (note?: string) => Promise<string | null>;
}
function resultToText(result: string | object | null | undefined): string {
@@ -120,6 +150,9 @@ export async function handleSessionCommand(opts: {
const command = cmdMsg
? extractSessionCommand(cmdMsg.content, triggerPattern)
: null;
const normalizedCommandText = cmdMsg
? normalizeSessionCommandText(cmdMsg.content, triggerPattern)
: '';
if (!command || !cmdMsg) return { handled: false };
@@ -164,6 +197,60 @@ export async function handleSessionCommand(opts: {
return { handled: true, success: true };
}
if (command === '/risk') {
const riskArg = normalizedCommandText.slice('/risk'.length).trim().toLowerCase();
const riskLevel =
riskArg === 'high' ? 'high' : riskArg === 'low' ? 'low' : null;
deps.advanceCursor(cmdMsg.timestamp);
await deps.sendMessage(
riskLevel
? (await deps.setTaskRiskLevel(riskLevel)) ??
'Risk is unavailable for this room.'
: 'Usage: /risk <low|high>',
);
return { handled: true, success: true };
}
if (command === '/plan') {
const payload = normalizedCommandText.slice('/plan'.length).trim();
const parts = payload.split('||').map((part) => part.trim());
deps.advanceCursor(cmdMsg.timestamp);
if (parts.length !== 3 || parts.some((part) => !part)) {
await deps.sendMessage(
'Usage: /plan <plan brief> || <acceptance criteria> || <risk summary>',
);
return { handled: true, success: true };
}
await deps.sendMessage(
(await deps.recordPlan({
planBrief: parts[0],
acceptanceCriteria: parts[1],
riskSummary: parts[2],
})) ?? 'Plan recording is unavailable for this room.',
);
return { handled: true, success: true };
}
if (command === '/approve-plan') {
deps.advanceCursor(cmdMsg.timestamp);
await deps.sendMessage(
(await deps.approvePlan()) ?? 'Plan approval is unavailable for this room.',
);
return { handled: true, success: true };
}
if (command === '/request-plan-changes') {
const note = normalizedCommandText
.slice('/request-plan-changes'.length)
.trim();
deps.advanceCursor(cmdMsg.timestamp);
await deps.sendMessage(
(await deps.requestPlanChanges(note || undefined)) ??
'Plan change requests are unavailable for this room.',
);
return { handled: true, success: true };
}
const cmdIndex = missedMessages.indexOf(cmdMsg);
const preCompactMsgs = missedMessages.slice(0, cmdIndex);