Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/cli/commands/execute.helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
assertValidKataName,
betStatusSymbol,
buildPreparedCycleOutputLines,
buildPreparedRunOutputLines,
formatDurationMs,
Expand All @@ -10,6 +11,8 @@ import {
parseCompletedRunArtifacts,
parseCompletedRunTokenUsage,
parseHintFlags,
resolveCompletionStatus,
resolveJsonFlag,
} from '@cli/commands/execute.helpers.js';

describe('execute helpers', () => {
Expand Down Expand Up @@ -421,4 +424,57 @@ describe('execute helpers', () => {
]);
});
});

describe('resolveJsonFlag', () => {
it('returns true when local json is set', () => {
expect(resolveJsonFlag(true, undefined)).toBe(true);
});

it('returns true when global json is set', () => {
expect(resolveJsonFlag(undefined, true)).toBe(true);
expect(resolveJsonFlag(false, true)).toBe(true);
});

it('returns true when both are set', () => {
expect(resolveJsonFlag(true, true)).toBe(true);
});

it('returns false when neither is set', () => {
expect(resolveJsonFlag(undefined, undefined)).toBe(false);
expect(resolveJsonFlag(false, false)).toBe(false);
});
});

describe('betStatusSymbol', () => {
it('returns looping arrow for in-progress', () => {
expect(betStatusSymbol('in-progress')).toBe('\u27F3');
});

it('returns check for complete', () => {
expect(betStatusSymbol('complete')).toBe('\u2713');
});

it('returns cross for failed', () => {
expect(betStatusSymbol('failed')).toBe('\u2717');
});

it('returns dot for unknown status', () => {
expect(betStatusSymbol('pending')).toBe('\u00B7');
expect(betStatusSymbol('')).toBe('\u00B7');
});
});

describe('resolveCompletionStatus', () => {
it('returns failed when flag is true', () => {
expect(resolveCompletionStatus(true)).toBe('failed');
});

it('returns complete when flag is false', () => {
expect(resolveCompletionStatus(false)).toBe('complete');
});

it('returns complete when flag is undefined', () => {
expect(resolveCompletionStatus(undefined)).toBe('complete');
});
});
});
15 changes: 15 additions & 0 deletions src/cli/commands/execute.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,21 @@ export function buildPreparedRunOutputLines(result: PreparedRunOutput, agentCont
];
}

export function resolveJsonFlag(localJson: boolean | undefined, globalJson: boolean | undefined): boolean {
return !!(localJson || globalJson);
}

export function betStatusSymbol(status: string): string {
if (status === 'in-progress') return '\u27F3';
if (status === 'complete') return '\u2713';
if (status === 'failed') return '\u2717';
return '\u00B7';
}

export function resolveCompletionStatus(failed: boolean | undefined): 'failed' | 'complete' {
return failed ? 'failed' : 'complete';
}

export function assertValidKataName(name: string): void {
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error(
Expand Down
17 changes: 10 additions & 7 deletions src/cli/commands/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CycleManager } from '@domain/services/cycle-manager.js';
import { SessionExecutionBridge } from '@infra/execution/session-bridge.js';
import {
assertValidKataName,
betStatusSymbol,
buildPreparedCycleOutputLines,
buildPreparedRunOutputLines,
formatDurationMs,
Expand All @@ -33,6 +34,8 @@ import {
parseCompletedRunArtifacts,
parseCompletedRunTokenUsage,
parseHintFlags,
resolveCompletionStatus,
resolveJsonFlag,
} from '@cli/commands/execute.helpers.js';
import { resolveRef } from '@cli/resolve-ref.js';
import { handleStatus, handleStats, parseCategoryFilter } from './status.js';
Expand Down Expand Up @@ -102,7 +105,7 @@ export function registerExecuteCommands(program: Command): void {
kataka?: string;
json?: boolean;
};
const isJson = !!(localOpts.json || ctx.globalOpts.json);
const isJson = resolveJsonFlag(localOpts.json, ctx.globalOpts.json);
const bridge = new SessionExecutionBridge(ctx.kataDir);
const agentId = localOpts.agent ?? localOpts.kataka;

Expand Down Expand Up @@ -143,7 +146,7 @@ export function registerExecuteCommands(program: Command): void {
}
console.log('');
for (const bet of result.bets) {
const status = bet.status === 'in-progress' ? '⟳' : bet.status === 'complete' ? '✓' : bet.status === 'failed' ? '✗' : '·';
const status = betStatusSymbol(bet.status);
console.log(` ${status} ${bet.betName} [${bet.status}]`);
if (bet.runId) {
console.log(` kansatsu: ${bet.kansatsuCount}, maki: ${bet.artifactCount}, kime: ${bet.decisionCount}`);
Expand Down Expand Up @@ -189,7 +192,7 @@ export function registerExecuteCommands(program: Command): void {
outputTokens?: number;
json?: boolean;
};
const isJson = !!(localOpts.json || ctx.globalOpts.json);
const isJson = resolveJsonFlag(localOpts.json, ctx.globalOpts.json);
const bridge = new SessionExecutionBridge(ctx.kataDir);

const parsedArtifacts = parseCompletedRunArtifacts(localOpts.artifacts);
Expand Down Expand Up @@ -226,14 +229,14 @@ export function registerExecuteCommands(program: Command): void {
if (isJson) {
console.log(JSON.stringify({
runId,
status: localOpts.failed ? 'failed' : 'complete',
status: resolveCompletionStatus(localOpts.failed),
...(tokenUsage ? { tokenUsage } : {}),
}));
} else {
const tokenLine = hasTokens
? ` (tokens: ${totalTokens ?? 0} total, ${tokenUsage?.inputTokens ?? 0} in, ${tokenUsage?.outputTokens ?? 0} out)`
: '';
console.log(`Run ${runId} marked as ${localOpts.failed ? 'failed' : 'complete'}.${tokenLine}`);
console.log(`Run ${runId} marked as ${resolveCompletionStatus(localOpts.failed)}.${tokenLine}`);
}
}));

Expand All @@ -245,7 +248,7 @@ export function registerExecuteCommands(program: Command): void {
.option('--json', 'Wrap output in a JSON object with a "agentContext" key')
.action(withCommandContext(async (ctx, runId: string) => {
const localOpts = ctx.cmd.opts() as { json?: boolean };
const isJson = !!(localOpts.json || ctx.globalOpts.json);
const isJson = resolveJsonFlag(localOpts.json, ctx.globalOpts.json);
const bridge = new SessionExecutionBridge(ctx.kataDir);

const agentContext = bridge.getAgentContext(runId);
Expand All @@ -267,7 +270,7 @@ export function registerExecuteCommands(program: Command): void {
.option('--json', 'Output as JSON')
.action(withCommandContext(async (ctx) => {
const localOpts = ctx.cmd.opts() as { bet: string; agent?: string; kataka?: string; json?: boolean };
const isJson = !!(localOpts.json || ctx.globalOpts.json);
const isJson = resolveJsonFlag(localOpts.json, ctx.globalOpts.json);
const bridge = new SessionExecutionBridge(ctx.kataDir);
const agentId = localOpts.agent ?? localOpts.kataka;

Expand Down
41 changes: 41 additions & 0 deletions src/features/cycle-management/cooldown-session.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
buildSynthesisInputRecord,
clampConfidenceWithDelta,
filterExecutionHistoryForCycle,
hasFailedCaptures,
isJsonFile,
isSynthesisPendingFile,
listCompletedBetDescriptions,
mapBridgeRunStatusToIncompleteStatus,
mapBridgeRunStatusToSyncedOutcome,
Expand Down Expand Up @@ -373,4 +376,42 @@ describe('cooldown-session helpers', () => {
])).toEqual(['Complete bet', 'Partial bet']);
});
});

describe('isJsonFile', () => {
it('returns true for .json files', () => {
expect(isJsonFile('run.json')).toBe(true);
expect(isJsonFile('pending-abc.json')).toBe(true);
});

it('returns false for non-.json files', () => {
expect(isJsonFile('readme.md')).toBe(false);
expect(isJsonFile('json')).toBe(false);
expect(isJsonFile('')).toBe(false);
});
});

describe('isSynthesisPendingFile', () => {
it('returns true for pending-*.json files', () => {
expect(isSynthesisPendingFile('pending-abc.json')).toBe(true);
expect(isSynthesisPendingFile('pending-123-456.json')).toBe(true);
});

it('returns false when prefix or suffix is wrong', () => {
expect(isSynthesisPendingFile('result-abc.json')).toBe(false);
expect(isSynthesisPendingFile('pending-abc.txt')).toBe(false);
expect(isSynthesisPendingFile('pending-.md')).toBe(false);
expect(isSynthesisPendingFile('')).toBe(false);
});
});

describe('hasFailedCaptures', () => {
it('returns true when failed count is positive', () => {
expect(hasFailedCaptures(1)).toBe(true);
expect(hasFailedCaptures(5)).toBe(true);
});

it('returns false when failed count is zero', () => {
expect(hasFailedCaptures(0)).toBe(false);
});
});
});
10 changes: 10 additions & 0 deletions src/features/cycle-management/cooldown-session.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,13 @@ export function listCompletedBetDescriptions(
.filter((bet) => bet.outcome === 'complete' || bet.outcome === 'partial')
.map((bet) => bet.description);
}

export { isJsonFile } from '@shared/lib/file-filters.js';

export function isSynthesisPendingFile(filename: string): boolean {
return filename.startsWith('pending-') && filename.endsWith('.json');
}

export function hasFailedCaptures(failed: number): boolean {
return failed > 0;
}
11 changes: 6 additions & 5 deletions src/features/cycle-management/cooldown-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ import {
shouldWarnOnIncompleteRuns,
shouldWriteDojoDiary,
shouldWriteDojoSession,
isJsonFile,
isSynthesisPendingFile,
hasFailedCaptures,
} from './cooldown-session.helpers.js';

/**
Expand Down Expand Up @@ -839,7 +842,7 @@ export class CooldownSession {

private cleanupStaleSynthesisInputs(synthesisDir: string, cycleId: string): void {
try {
const existing = readdirSync(synthesisDir).filter((file) => file.startsWith('pending-') && file.endsWith('.json'));
const existing = readdirSync(synthesisDir).filter(isSynthesisPendingFile);
for (const file of existing) {
this.removeStaleSynthesisInputFile(synthesisDir, file, cycleId);
}
Expand Down Expand Up @@ -871,7 +874,6 @@ export class CooldownSession {
*/
private loadBridgeRunIdsByBetId(cycleId: string, bridgeRunsDir: string): Map<string, string> {
const result = new Map<string, string>();
if (!existsSync(bridgeRunsDir)) return result;

for (const file of this.listJsonFiles(bridgeRunsDir)) {
const meta = this.readBridgeRunMeta(join(bridgeRunsDir, file));
Expand All @@ -885,7 +887,7 @@ export class CooldownSession {

private listJsonFiles(dir: string): string[] {
try {
return readdirSync(dir).filter((file) => file.endsWith('.json'));
return readdirSync(dir).filter(isJsonFile);
} catch {
return [];
}
Expand Down Expand Up @@ -937,7 +939,6 @@ export class CooldownSession {
runId: string,
): BetOutcomeRecord['outcome'] | undefined {
const bridgeRunPath = join(bridgeRunsDir, `${runId}.json`);
if (!existsSync(bridgeRunPath)) return undefined;
const status = this.readBridgeRunMeta(bridgeRunPath)?.status;
return mapBridgeRunStatusToSyncedOutcome(status);
}
Expand Down Expand Up @@ -988,7 +989,7 @@ export class CooldownSession {
*/
private captureCooldownLearnings(report: CooldownReport): number {
const attempts = this.captureCooldownLearningDrafts(report);
if (attempts.failed > 0) {
if (hasFailedCaptures(attempts.failed)) {
logger.warn(`${attempts.failed} of ${attempts.captured + attempts.failed} cooldown learnings failed to capture. Check previous warnings for details.`);
}

Expand Down
Loading
Loading