Extract paired verdict signal helpers
This commit is contained in:
@@ -73,7 +73,7 @@ import {
|
|||||||
isBotMessageSourceKind,
|
isBotMessageSourceKind,
|
||||||
resolveInjectedMessageSourceKind,
|
resolveInjectedMessageSourceKind,
|
||||||
} from './message-source.js';
|
} from './message-source.js';
|
||||||
import { parseVisibleVerdict } from './paired-execution-context-shared.js';
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
|
|
||||||
export function isTerminalStatusMessage(text: string): boolean {
|
export function isTerminalStatusMessage(text: string): boolean {
|
||||||
return parseVisibleVerdict(text) !== 'continue';
|
return parseVisibleVerdict(text) !== 'continue';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createProducedWorkItem } from './db.js';
|
import { createProducedWorkItem } from './db.js';
|
||||||
import type { WorkItem } from './db/work-items.js';
|
import type { WorkItem } from './db/work-items.js';
|
||||||
import { deliverOpenWorkItem } from './message-runtime-delivery.js';
|
import { deliverOpenWorkItem } from './message-runtime-delivery.js';
|
||||||
import { parseVisibleVerdict } from './paired-execution-context-shared.js';
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
import { resolveChannelForDeliveryRole } from './router.js';
|
import { resolveChannelForDeliveryRole } from './router.js';
|
||||||
import type {
|
import type {
|
||||||
Channel,
|
Channel,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
completePairedExecutionContext,
|
completePairedExecutionContext,
|
||||||
type PreparedPairedExecutionContext,
|
type PreparedPairedExecutionContext,
|
||||||
} from './paired-execution-context.js';
|
} from './paired-execution-context.js';
|
||||||
import { parseVisibleVerdict } from './paired-execution-context-shared.js';
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
import { resolvePairedFollowUpQueueAction } from './message-agent-executor-rules.js';
|
import { resolvePairedFollowUpQueueAction } from './message-agent-executor-rules.js';
|
||||||
import { enqueuePairedFollowUpAfterEvent } from './message-runtime-follow-up.js';
|
import { enqueuePairedFollowUpAfterEvent } from './message-runtime-follow-up.js';
|
||||||
import type { PairedTurnIdentity } from './paired-turn-identity.js';
|
import type { PairedTurnIdentity } from './paired-turn-identity.js';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
resolveFollowUpDispatch,
|
resolveFollowUpDispatch,
|
||||||
resolveNextTurnAction,
|
resolveNextTurnAction,
|
||||||
} from './message-runtime-rules.js';
|
} from './message-runtime-rules.js';
|
||||||
import { parseVisibleVerdict } from './paired-execution-context-shared.js';
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
import type { PairedRoomRole, PairedTaskStatus } from './types.js';
|
import type { PairedRoomRole, PairedTaskStatus } from './types.js';
|
||||||
export {
|
export {
|
||||||
isRetryableClaudeSessionFailureAttempt,
|
isRetryableClaudeSessionFailureAttempt,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { getPairedTurnOutputs } from './db.js';
|
import { getPairedTurnOutputs } from './db.js';
|
||||||
import { type VisibleVerdict } from './paired-execution-context-shared.js';
|
import {
|
||||||
import { resolveStoredVisibleVerdict } from './paired-verdict.js';
|
resolveStoredVisibleVerdict,
|
||||||
|
type VisibleVerdict,
|
||||||
|
} from './paired-verdict.js';
|
||||||
import {
|
import {
|
||||||
matchesExpectedPairedFollowUpIntent,
|
matchesExpectedPairedFollowUpIntent,
|
||||||
resolveFollowUpDispatch,
|
resolveFollowUpDispatch,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { normalizeStoredSeqCursor } from './message-cursor.js';
|
|||||||
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
||||||
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
||||||
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
||||||
import type { VisibleVerdict } from './paired-execution-context-shared.js';
|
import type { VisibleVerdict } from './paired-verdict.js';
|
||||||
import {
|
import {
|
||||||
hasReviewerLease,
|
hasReviewerLease,
|
||||||
resolveLeaseServiceId,
|
resolveLeaseServiceId,
|
||||||
|
|||||||
109
src/paired-completion-signals.ts
Normal file
109
src/paired-completion-signals.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { VisibleVerdict } from './paired-verdict.js';
|
||||||
|
|
||||||
|
export type CompletionSignal =
|
||||||
|
| { kind: 'request_reviewer'; resetStatusToActive: boolean }
|
||||||
|
| { kind: 'request_owner_finalize' }
|
||||||
|
| { kind: 'request_owner_changes' }
|
||||||
|
| { kind: 'request_arbiter' }
|
||||||
|
| { kind: 'complete'; completionReason: 'done' | 'escalated' }
|
||||||
|
| { kind: 'preserve_review_ready' };
|
||||||
|
|
||||||
|
export function resolveOwnerCompletionSignal(args: {
|
||||||
|
phase: 'normal' | 'finalize';
|
||||||
|
visibleVerdict: VisibleVerdict;
|
||||||
|
hasChangesSinceApproval?: boolean | null;
|
||||||
|
roundTripCount?: number;
|
||||||
|
deadlockThreshold?: number;
|
||||||
|
}): CompletionSignal {
|
||||||
|
const {
|
||||||
|
phase,
|
||||||
|
visibleVerdict,
|
||||||
|
hasChangesSinceApproval = false,
|
||||||
|
roundTripCount = 0,
|
||||||
|
deadlockThreshold = Number.POSITIVE_INFINITY,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
if (visibleVerdict === 'blocked' || visibleVerdict === 'needs_context') {
|
||||||
|
return { kind: 'request_arbiter' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase === 'normal') {
|
||||||
|
return {
|
||||||
|
kind: 'request_reviewer',
|
||||||
|
resetStatusToActive: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleVerdict === 'step_done') {
|
||||||
|
return {
|
||||||
|
kind: 'request_reviewer',
|
||||||
|
resetStatusToActive: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsReReview =
|
||||||
|
visibleVerdict === 'done_with_concerns' || hasChangesSinceApproval === true;
|
||||||
|
|
||||||
|
if (needsReReview) {
|
||||||
|
if (roundTripCount >= deadlockThreshold) {
|
||||||
|
return { kind: 'request_arbiter' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: 'request_reviewer',
|
||||||
|
resetStatusToActive: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'complete',
|
||||||
|
completionReason: 'done',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewerCompletionSignal(args: {
|
||||||
|
visibleVerdict: VisibleVerdict;
|
||||||
|
roundTripCount: number;
|
||||||
|
deadlockThreshold: number;
|
||||||
|
}): CompletionSignal {
|
||||||
|
const { visibleVerdict, roundTripCount, deadlockThreshold } = args;
|
||||||
|
|
||||||
|
switch (visibleVerdict) {
|
||||||
|
case 'task_done':
|
||||||
|
case 'done':
|
||||||
|
return { kind: 'request_owner_finalize' };
|
||||||
|
case 'step_done':
|
||||||
|
return { kind: 'request_owner_changes' };
|
||||||
|
case 'blocked':
|
||||||
|
case 'needs_context':
|
||||||
|
return { kind: 'request_arbiter' };
|
||||||
|
case 'done_with_concerns':
|
||||||
|
case 'continue':
|
||||||
|
default:
|
||||||
|
if (roundTripCount >= deadlockThreshold) {
|
||||||
|
return { kind: 'request_arbiter' };
|
||||||
|
}
|
||||||
|
return { kind: 'request_owner_changes' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewerFailureSignal(args: {
|
||||||
|
visibleVerdict: VisibleVerdict;
|
||||||
|
}): CompletionSignal {
|
||||||
|
const { visibleVerdict } = args;
|
||||||
|
|
||||||
|
switch (visibleVerdict) {
|
||||||
|
case 'task_done':
|
||||||
|
case 'done':
|
||||||
|
return { kind: 'request_owner_finalize' };
|
||||||
|
case 'blocked':
|
||||||
|
case 'needs_context':
|
||||||
|
return {
|
||||||
|
kind: 'complete',
|
||||||
|
completionReason: 'escalated',
|
||||||
|
};
|
||||||
|
case 'done_with_concerns':
|
||||||
|
case 'continue':
|
||||||
|
default:
|
||||||
|
return { kind: 'preserve_review_ready' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ARBITER_DEADLOCK_THRESHOLD } from './config.js';
|
import { ARBITER_DEADLOCK_THRESHOLD } from './config.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import {
|
import { transitionPairedTaskStatus } from './paired-execution-context-shared.js';
|
||||||
classifyArbiterVerdict,
|
import { classifyArbiterVerdict } from './paired-verdict.js';
|
||||||
transitionPairedTaskStatus,
|
|
||||||
} from './paired-execution-context-shared.js';
|
|
||||||
import type { PairedTask } from './types.js';
|
import type { PairedTask } from './types.js';
|
||||||
|
|
||||||
export function handleFailedArbiterExecution(args: {
|
export function handleFailedArbiterExecution(args: {
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import { markPairedTaskReviewReady } from './paired-workspace-manager.js';
|
|||||||
import {
|
import {
|
||||||
applyPairedTaskPatch,
|
applyPairedTaskPatch,
|
||||||
hasCodeChangesSinceRef,
|
hasCodeChangesSinceRef,
|
||||||
parseVisibleVerdict,
|
|
||||||
requestArbiterOrEscalate,
|
requestArbiterOrEscalate,
|
||||||
resolveOwnerCompletionSignal,
|
|
||||||
resolveCanonicalSourceRef,
|
resolveCanonicalSourceRef,
|
||||||
transitionPairedTaskStatus,
|
transitionPairedTaskStatus,
|
||||||
} from './paired-execution-context-shared.js';
|
} from './paired-execution-context-shared.js';
|
||||||
|
import { resolveOwnerCompletionSignal } from './paired-completion-signals.js';
|
||||||
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
import type { PairedTask } from './types.js';
|
import type { PairedTask } from './types.js';
|
||||||
|
|
||||||
type OwnerFinalizeOutcome = 'stop' | 're_review';
|
type OwnerFinalizeOutcome = 'stop' | 're_review';
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import { ARBITER_DEADLOCK_THRESHOLD } from './config.js';
|
|||||||
import { getPairedWorkspace } from './db.js';
|
import { getPairedWorkspace } from './db.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import {
|
import {
|
||||||
parseVisibleVerdict,
|
|
||||||
requestArbiterOrEscalate,
|
requestArbiterOrEscalate,
|
||||||
resolveReviewerCompletionSignal,
|
|
||||||
resolveReviewerFailureSignal,
|
|
||||||
resolveCanonicalSourceRef,
|
resolveCanonicalSourceRef,
|
||||||
transitionPairedTaskStatus,
|
transitionPairedTaskStatus,
|
||||||
} from './paired-execution-context-shared.js';
|
} from './paired-execution-context-shared.js';
|
||||||
|
import {
|
||||||
|
resolveReviewerCompletionSignal,
|
||||||
|
resolveReviewerFailureSignal,
|
||||||
|
} from './paired-completion-signals.js';
|
||||||
|
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||||
import type { PairedTask } from './types.js';
|
import type { PairedTask } from './types.js';
|
||||||
|
|
||||||
export function handleFailedReviewerExecution(args: {
|
export function handleFailedReviewerExecution(args: {
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseVisibleVerdict,
|
|
||||||
resolveOwnerCompletionSignal,
|
resolveOwnerCompletionSignal,
|
||||||
resolveReviewerCompletionSignal,
|
resolveReviewerCompletionSignal,
|
||||||
resolveReviewerFailureSignal,
|
resolveReviewerFailureSignal,
|
||||||
} from './paired-execution-context-shared.js';
|
} from './paired-completion-signals.js';
|
||||||
|
|
||||||
describe('paired execution context shared verdict helpers', () => {
|
describe('paired execution context shared verdict helpers', () => {
|
||||||
it('parses visible verdicts from the first summary line only', () => {
|
|
||||||
expect(parseVisibleVerdict('STEP_DONE\nmore to do')).toBe('step_done');
|
|
||||||
expect(parseVisibleVerdict('TASK_DONE\nall done')).toBe('task_done');
|
|
||||||
expect(
|
|
||||||
parseVisibleVerdict(
|
|
||||||
'DONE_WITH_CONCERNS\n\nfollow-up detail that should not affect parsing',
|
|
||||||
),
|
|
||||||
).toBe('done_with_concerns');
|
|
||||||
expect(parseVisibleVerdict('BLOCKED\nextra detail')).toBe('blocked');
|
|
||||||
expect(parseVisibleVerdict('random prose')).toBe('continue');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps normal owner completion verdicts to reviewer or arbiter signals', () => {
|
it('maps normal owner completion verdicts to reviewer or arbiter signals', () => {
|
||||||
expect(
|
expect(
|
||||||
resolveOwnerCompletionSignal({
|
resolveOwnerCompletionSignal({
|
||||||
|
|||||||
@@ -3,142 +3,19 @@ import { execFileSync } from 'child_process';
|
|||||||
import { isArbiterEnabled } from './config.js';
|
import { isArbiterEnabled } from './config.js';
|
||||||
import { updatePairedTaskIfUnchanged } from './db.js';
|
import { updatePairedTaskIfUnchanged } from './db.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { parseVisibleVerdict, type VisibleVerdict } from './paired-verdict.js';
|
|
||||||
import type { PairedTaskStatus } from './types.js';
|
import type { PairedTaskStatus } from './types.js';
|
||||||
|
|
||||||
export type CompletionSignal =
|
export {
|
||||||
| { kind: 'request_reviewer'; resetStatusToActive: boolean }
|
resolveOwnerCompletionSignal,
|
||||||
| { kind: 'request_owner_finalize' }
|
resolveReviewerCompletionSignal,
|
||||||
| { kind: 'request_owner_changes' }
|
resolveReviewerFailureSignal,
|
||||||
| { kind: 'request_arbiter' }
|
} from './paired-completion-signals.js';
|
||||||
| { kind: 'complete'; completionReason: 'done' | 'escalated' }
|
export type { CompletionSignal } from './paired-completion-signals.js';
|
||||||
| { kind: 'preserve_review_ready' };
|
export {
|
||||||
|
classifyArbiterVerdict,
|
||||||
export { parseVisibleVerdict };
|
parseVisibleVerdict,
|
||||||
export type { VisibleVerdict };
|
} from './paired-verdict.js';
|
||||||
|
export type { ArbiterVerdictResult, VisibleVerdict } from './paired-verdict.js';
|
||||||
export function resolveOwnerCompletionSignal(args: {
|
|
||||||
phase: 'normal' | 'finalize';
|
|
||||||
visibleVerdict: VisibleVerdict;
|
|
||||||
hasChangesSinceApproval?: boolean | null;
|
|
||||||
roundTripCount?: number;
|
|
||||||
deadlockThreshold?: number;
|
|
||||||
}): CompletionSignal {
|
|
||||||
const {
|
|
||||||
phase,
|
|
||||||
visibleVerdict,
|
|
||||||
hasChangesSinceApproval = false,
|
|
||||||
roundTripCount = 0,
|
|
||||||
deadlockThreshold = Number.POSITIVE_INFINITY,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
if (visibleVerdict === 'blocked' || visibleVerdict === 'needs_context') {
|
|
||||||
return { kind: 'request_arbiter' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phase === 'normal') {
|
|
||||||
return {
|
|
||||||
kind: 'request_reviewer',
|
|
||||||
resetStatusToActive: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visibleVerdict === 'step_done') {
|
|
||||||
return {
|
|
||||||
kind: 'request_reviewer',
|
|
||||||
resetStatusToActive: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const needsReReview =
|
|
||||||
visibleVerdict === 'done_with_concerns' || hasChangesSinceApproval === true;
|
|
||||||
|
|
||||||
if (needsReReview) {
|
|
||||||
if (roundTripCount >= deadlockThreshold) {
|
|
||||||
return { kind: 'request_arbiter' };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
kind: 'request_reviewer',
|
|
||||||
resetStatusToActive: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: 'complete',
|
|
||||||
completionReason: 'done',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveReviewerCompletionSignal(args: {
|
|
||||||
visibleVerdict: VisibleVerdict;
|
|
||||||
roundTripCount: number;
|
|
||||||
deadlockThreshold: number;
|
|
||||||
}): CompletionSignal {
|
|
||||||
const { visibleVerdict, roundTripCount, deadlockThreshold } = args;
|
|
||||||
|
|
||||||
switch (visibleVerdict) {
|
|
||||||
case 'task_done':
|
|
||||||
case 'done':
|
|
||||||
return { kind: 'request_owner_finalize' };
|
|
||||||
case 'step_done':
|
|
||||||
return { kind: 'request_owner_changes' };
|
|
||||||
case 'blocked':
|
|
||||||
case 'needs_context':
|
|
||||||
return { kind: 'request_arbiter' };
|
|
||||||
case 'done_with_concerns':
|
|
||||||
case 'continue':
|
|
||||||
default:
|
|
||||||
if (roundTripCount >= deadlockThreshold) {
|
|
||||||
return { kind: 'request_arbiter' };
|
|
||||||
}
|
|
||||||
return { kind: 'request_owner_changes' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveReviewerFailureSignal(args: {
|
|
||||||
visibleVerdict: VisibleVerdict;
|
|
||||||
}): CompletionSignal {
|
|
||||||
const { visibleVerdict } = args;
|
|
||||||
|
|
||||||
switch (visibleVerdict) {
|
|
||||||
case 'task_done':
|
|
||||||
case 'done':
|
|
||||||
return { kind: 'request_owner_finalize' };
|
|
||||||
case 'blocked':
|
|
||||||
case 'needs_context':
|
|
||||||
return {
|
|
||||||
kind: 'complete',
|
|
||||||
completionReason: 'escalated',
|
|
||||||
};
|
|
||||||
case 'done_with_concerns':
|
|
||||||
case 'continue':
|
|
||||||
default:
|
|
||||||
return { kind: 'preserve_review_ready' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArbiterVerdictResult =
|
|
||||||
| 'proceed'
|
|
||||||
| 'revise'
|
|
||||||
| 'reset'
|
|
||||||
| 'escalate'
|
|
||||||
| 'unknown';
|
|
||||||
|
|
||||||
export function classifyArbiterVerdict(
|
|
||||||
summary: string | null | undefined,
|
|
||||||
): ArbiterVerdictResult {
|
|
||||||
if (!summary) return 'unknown';
|
|
||||||
const cleaned = summary.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
|
||||||
if (!cleaned) return 'unknown';
|
|
||||||
const firstLine = cleaned.split('\n')[0].trim();
|
|
||||||
const verdictMatch = firstLine.match(
|
|
||||||
/\*{0,2}(?:VERDICT\s*[:—-]\s*)?(PROCEED|REVISE|RESET|ESCALATE)\*{0,2}/i,
|
|
||||||
);
|
|
||||||
if (verdictMatch) {
|
|
||||||
return verdictMatch[1].toLowerCase() as ArbiterVerdictResult;
|
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveCanonicalSourceRef(workDir: string): string {
|
export function resolveCanonicalSourceRef(workDir: string): string {
|
||||||
const treeHash = resolveCanonicalTreeHash(workDir);
|
const treeHash = resolveCanonicalTreeHash(workDir);
|
||||||
|
|||||||
34
src/paired-verdict.test.ts
Normal file
34
src/paired-verdict.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
classifyArbiterVerdict,
|
||||||
|
parseVisibleVerdict,
|
||||||
|
} from './paired-verdict.js';
|
||||||
|
|
||||||
|
describe('paired verdict parser', () => {
|
||||||
|
it('parses visible verdicts from the first summary line only', () => {
|
||||||
|
expect(parseVisibleVerdict('STEP_DONE\nmore to do')).toBe('step_done');
|
||||||
|
expect(parseVisibleVerdict('TASK_DONE\nall done')).toBe('task_done');
|
||||||
|
expect(
|
||||||
|
parseVisibleVerdict(
|
||||||
|
'DONE_WITH_CONCERNS\n\nfollow-up detail that should not affect parsing',
|
||||||
|
),
|
||||||
|
).toBe('done_with_concerns');
|
||||||
|
expect(parseVisibleVerdict('BLOCKED\nextra detail')).toBe('blocked');
|
||||||
|
expect(parseVisibleVerdict('random prose')).toBe('continue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('classifies arbiter verdicts from the first visible line', () => {
|
||||||
|
expect(classifyArbiterVerdict('PROCEED\ncontinue')).toBe('proceed');
|
||||||
|
expect(classifyArbiterVerdict('**VERDICT: REVISE**\nfix it')).toBe(
|
||||||
|
'revise',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
classifyArbiterVerdict(
|
||||||
|
'<internal>private notes</internal>\nVERDICT — RESET\nrestart',
|
||||||
|
),
|
||||||
|
).toBe('reset');
|
||||||
|
expect(classifyArbiterVerdict('ESCALATE\nblocked')).toBe('escalate');
|
||||||
|
expect(classifyArbiterVerdict('no verdict here')).toBe('unknown');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,13 @@ export type VisibleVerdict =
|
|||||||
| 'needs_context'
|
| 'needs_context'
|
||||||
| 'continue';
|
| 'continue';
|
||||||
|
|
||||||
|
export type ArbiterVerdictResult =
|
||||||
|
| 'proceed'
|
||||||
|
| 'revise'
|
||||||
|
| 'reset'
|
||||||
|
| 'escalate'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
export function parseVisibleVerdict(
|
export function parseVisibleVerdict(
|
||||||
summary: string | null | undefined,
|
summary: string | null | undefined,
|
||||||
): VisibleVerdict {
|
): VisibleVerdict {
|
||||||
@@ -26,6 +33,22 @@ export function parseVisibleVerdict(
|
|||||||
return 'continue';
|
return 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function classifyArbiterVerdict(
|
||||||
|
summary: string | null | undefined,
|
||||||
|
): ArbiterVerdictResult {
|
||||||
|
if (!summary) return 'unknown';
|
||||||
|
const cleaned = summary.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
||||||
|
if (!cleaned) return 'unknown';
|
||||||
|
const firstLine = cleaned.split('\n')[0].trim();
|
||||||
|
const verdictMatch = firstLine.match(
|
||||||
|
/\*{0,2}(?:VERDICT\s*[:—-]\s*)?(PROCEED|REVISE|RESET|ESCALATE)\*{0,2}/i,
|
||||||
|
);
|
||||||
|
if (verdictMatch) {
|
||||||
|
return verdictMatch[1].toLowerCase() as ArbiterVerdictResult;
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveStoredVisibleVerdict(args: {
|
export function resolveStoredVisibleVerdict(args: {
|
||||||
verdict?: VisibleVerdict | null;
|
verdict?: VisibleVerdict | null;
|
||||||
outputText?: string | null;
|
outputText?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user