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
1 change: 1 addition & 0 deletions src/acceptance/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@domain/rules/cycle-rules.steps.js';
import '@domain/services/cycle-manager.steps.js';
import '@features/cycle-management/bridge-run-syncer.steps.js';
import '@features/cycle-management/cooldown-belt-computer.steps.js';
import '@features/cycle-management/cycle-activation-name-resolver.steps.js';
import '@infra/execution/session-bridge.steps.js';
73 changes: 73 additions & 0 deletions src/features/cycle-management/cooldown-belt-computer.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Feature: Belt and agent confidence computation during cooldown
After a cooldown phase completes, the practitioner's belt level is
recalculated and per-agent confidence profiles are updated so that
progress tracking always reflects the latest cycle outcomes.

Background:
Given the cooldown environment is ready

# ── Belt evaluation ────────────────────────────────────────────

Scenario: belt advances when the practitioner levels up
Given belt evaluation is enabled
And the practitioner has earned advancement from "go-kyu" to "yon-kyu"
When belt evaluation runs
Then the belt result shows a level-up to "yon-kyu"
And belt advancement is logged

Scenario: belt stays steady when no level-up occurs
Given belt evaluation is enabled
And the practitioner remains steady at "go-kyu"
When belt evaluation runs
Then the belt result shows steady at "go-kyu"
And no belt advancement is logged

Scenario: belt evaluation is skipped when not enabled
Given belt evaluation is not enabled
When belt evaluation runs
Then no belt result is returned

Scenario: belt evaluation is skipped when project state is unavailable
Given belt evaluation is enabled without project state
When belt evaluation runs
Then no belt result is returned

Scenario: belt evaluation failure does not abort cooldown
Given belt evaluation is enabled
And belt evaluation will fail with an internal error
When belt evaluation runs
Then no belt result is returned
And a warning is logged about belt computation failure
And cooldown continues normally

# ── Agent confidence tracking ──────────────────────────────────

Scenario: confidence is computed for each registered agent
Given agent confidence tracking is enabled
And agents "Alpha" and "Beta" are registered
When agent confidence computation runs
Then confidence is computed for agent "Alpha"
And confidence is computed for agent "Beta"

Scenario: agent confidence supports legacy kataka aliases
Given agent confidence tracking is enabled via legacy kataka configuration
And agent "Legacy" is registered in the kataka directory
When agent confidence computation runs
Then confidence is computed for agent "Legacy"

Scenario: agent confidence is skipped when tracking is not enabled
Given agent confidence tracking is not enabled
When agent confidence computation runs
Then no confidence computations occur

Scenario: agent confidence is skipped when no agent registry is available
Given agent confidence tracking is enabled without an agent registry
When agent confidence computation runs
Then no confidence computations occur

Scenario: agent confidence failure does not abort cooldown
Given agent confidence tracking is enabled
And the agent registry contains invalid data
When agent confidence computation runs
Then a warning is logged about agent confidence failure
And cooldown continues normally
315 changes: 315 additions & 0 deletions src/features/cycle-management/cooldown-belt-computer.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { Given, Then, When, QuickPickleWorld } from 'quickpickle';
import { expect, vi } from 'vitest';
import { logger } from '@shared/lib/logger.js';
import { CooldownBeltComputer, type CooldownBeltDeps } from './cooldown-belt-computer.js';
import type { BeltComputeResult } from '@features/belt/belt-calculator.js';
import type { BeltLevel } from '@domain/types/belt.js';
import type { BeltCalculator } from '@features/belt/belt-calculator.js';
import type { KataAgentConfidenceCalculator } from '@features/kata-agent/kata-agent-confidence-calculator.js';

type ComputeAndStoreFn = BeltCalculator['computeAndStore'];
type ComputeFn = KataAgentConfidenceCalculator['compute'];

// ── World ────────────────────────────────────────────────────

interface CooldownBeltComputerWorld extends QuickPickleWorld {
tmpDir: string;
beltCalculatorSpy?: { computeAndStore: ReturnType<typeof vi.fn<ComputeAndStoreFn>> };
projectStateFile?: string;
agentConfidenceCalculatorSpy?: { compute: ReturnType<typeof vi.fn<ComputeFn>> };
katakaConfidenceCalculatorSpy?: { compute: ReturnType<typeof vi.fn<ComputeFn>> };
agentDir?: string;
katakaDir?: string;
computer?: CooldownBeltComputer;
beltResult?: BeltComputeResult;
loggerInfoSpy: ReturnType<typeof vi.fn>;
loggerWarnSpy: ReturnType<typeof vi.fn>;
lastError?: Error;
}

// ── Helpers ──────────────────────────────────────────────────

function writeAgentRecord(dir: string, name: string): string {
mkdirSync(dir, { recursive: true });
const id = randomUUID();
writeFileSync(
join(dir, `${id}.json`),
JSON.stringify({
id,
name,
role: 'executor',
skills: [],
createdAt: new Date().toISOString(),
active: true,
}),
);
return id;
}

function buildComputer(world: CooldownBeltComputerWorld): CooldownBeltComputer {
const deps: CooldownBeltDeps = {
beltCalculator: world.beltCalculatorSpy,
projectStateFile: world.projectStateFile,
agentConfidenceCalculator: world.agentConfidenceCalculatorSpy,
katakaConfidenceCalculator: world.katakaConfidenceCalculatorSpy,
agentDir: world.agentDir,
katakaDir: world.katakaDir,
};
return new CooldownBeltComputer(deps);
}

// ── Background ───────────────────────────────────────────────

Given(
'the cooldown environment is ready',
(world: CooldownBeltComputerWorld) => {
world.tmpDir = mkdtempSync(join(tmpdir(), 'cbc-'));
world.loggerInfoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {});
world.loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
},
);

// ── Given: belt evaluation ───────────────────────────────────

Given(
'belt evaluation is enabled',
(world: CooldownBeltComputerWorld) => {
world.projectStateFile = join(world.tmpDir, 'project-state.json');
world.beltCalculatorSpy = { computeAndStore: vi.fn<ComputeAndStoreFn>() };
},
);

Given(
'the practitioner has earned advancement from {string} to {string}',
(world: CooldownBeltComputerWorld, from: string, to: string) => {
world.beltCalculatorSpy!.computeAndStore.mockReturnValue({
belt: to as BeltLevel,
previous: from as BeltLevel,
leveledUp: true,
} as BeltComputeResult);
},
);

Given(
'the practitioner remains steady at {string}',
(world: CooldownBeltComputerWorld, level: string) => {
world.beltCalculatorSpy!.computeAndStore.mockReturnValue({
belt: level as BeltLevel,
previous: level as BeltLevel,
leveledUp: false,
} as BeltComputeResult);
},
);

Given(
'belt evaluation is not enabled',
(_world: CooldownBeltComputerWorld) => {
// No belt calculator or project state β€” both left undefined
},
);

Given(
'belt evaluation is enabled without project state',
(world: CooldownBeltComputerWorld) => {
world.beltCalculatorSpy = { computeAndStore: vi.fn<ComputeAndStoreFn>() };
// projectStateFile left undefined
},
);

Given(
'belt evaluation will fail with an internal error',
(world: CooldownBeltComputerWorld) => {
world.beltCalculatorSpy!.computeAndStore.mockImplementation(() => {
throw new Error('Simulated belt failure');
});
},
);

// ── Given: agent confidence tracking ─────────────────────────

Given(
'agent confidence tracking is enabled',
(world: CooldownBeltComputerWorld) => {
world.agentDir = join(world.tmpDir, 'agents');
mkdirSync(world.agentDir, { recursive: true });
world.agentConfidenceCalculatorSpy = { compute: vi.fn<ComputeFn>() };
},
);

Given(
'agents {string} and {string} are registered',
(world: CooldownBeltComputerWorld, name1: string, name2: string) => {
writeAgentRecord(world.agentDir!, name1);
writeAgentRecord(world.agentDir!, name2);
},
);

Given(
'agent confidence tracking is enabled via legacy kataka configuration',
(world: CooldownBeltComputerWorld) => {
world.katakaDir = join(world.tmpDir, 'kataka-agents');
mkdirSync(world.katakaDir, { recursive: true });
world.katakaConfidenceCalculatorSpy = { compute: vi.fn<ComputeFn>() };
},
);

Given(
'agent {string} is registered in the kataka directory',
(world: CooldownBeltComputerWorld, name: string) => {
writeAgentRecord(world.katakaDir!, name);
},
);

Given(
'agent confidence tracking is not enabled',
(_world: CooldownBeltComputerWorld) => {
// No calculator or directory β€” both left undefined
},
);

Given(
'agent confidence tracking is enabled without an agent registry',
(world: CooldownBeltComputerWorld) => {
world.agentConfidenceCalculatorSpy = { compute: vi.fn<ComputeFn>() };
// agentDir left undefined
},
);

Given(
'the agent registry contains invalid data',
(world: CooldownBeltComputerWorld) => {
// Point agentDir at a file instead of a directory β€” KataAgentRegistry will fail
const brokenPath = join(world.tmpDir, 'not-a-directory.json');
writeFileSync(brokenPath, '{}');
world.agentDir = brokenPath;
},
);

// ── When ─────────────────────────────────────────────────────

When(
'belt evaluation runs',
(world: CooldownBeltComputerWorld) => {
world.computer = buildComputer(world);
try {
world.beltResult = world.computer.compute();
} catch (err) {
world.lastError = err as Error;
}
},
);

When(
'agent confidence computation runs',
(world: CooldownBeltComputerWorld) => {
world.computer = buildComputer(world);
try {
world.computer.computeAgentConfidence();
} catch (err) {
world.lastError = err as Error;
}
},
);

// ── Then: belt assertions ────────────────────────────────────

Then(
'the belt result shows a level-up to {string}',
(world: CooldownBeltComputerWorld, expectedBelt: string) => {
expect(world.lastError).toBeUndefined();
expect(world.beltResult).toBeDefined();
expect(world.beltResult!.belt).toBe(expectedBelt);
expect(world.beltResult!.leveledUp).toBe(true);
},
);

Then(
'the belt result shows steady at {string}',
(world: CooldownBeltComputerWorld, expectedBelt: string) => {
expect(world.lastError).toBeUndefined();
expect(world.beltResult).toBeDefined();
expect(world.beltResult!.belt).toBe(expectedBelt);
expect(world.beltResult!.leveledUp).toBe(false);
},
);

Then(
'belt advancement is logged',
(world: CooldownBeltComputerWorld) => {
expect(world.loggerInfoSpy).toHaveBeenCalled();
const msg = world.loggerInfoSpy.mock.calls[0]![0] as string;
expect(msg).toContain('Belt advanced');
},
);

Then(
'no belt advancement is logged',
(world: CooldownBeltComputerWorld) => {
const infoCalls = world.loggerInfoSpy.mock.calls;
const beltMessages = infoCalls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('Belt advanced'),
);
expect(beltMessages).toHaveLength(0);
},
);

Then(
'no belt result is returned',
(world: CooldownBeltComputerWorld) => {
expect(world.lastError).toBeUndefined();
expect(world.beltResult).toBeUndefined();
},
);

// ── Then: agent confidence assertions ────────────────────────

Then(
'confidence is computed for agent {string}',
(world: CooldownBeltComputerWorld, agentName: string) => {
expect(world.lastError).toBeUndefined();
const calculator = world.agentConfidenceCalculatorSpy ?? world.katakaConfidenceCalculatorSpy;
expect(calculator).toBeDefined();
const calls = calculator!.compute.mock.calls as [string, string][];
const match = calls.find(([, name]) => name === agentName);
expect(match).toBeDefined();
},
);

Then(
'no confidence computations occur',
(world: CooldownBeltComputerWorld) => {
expect(world.lastError).toBeUndefined();
if (world.agentConfidenceCalculatorSpy) {
expect(world.agentConfidenceCalculatorSpy.compute).not.toHaveBeenCalled();
}
if (world.katakaConfidenceCalculatorSpy) {
expect(world.katakaConfidenceCalculatorSpy.compute).not.toHaveBeenCalled();
}
},
);

// ── Then: safety assertions ──────────────────────────────────

Then(
'a warning is logged about belt computation failure',
(world: CooldownBeltComputerWorld) => {
expect(world.loggerWarnSpy).toHaveBeenCalled();
const msg = world.loggerWarnSpy.mock.calls[0]![0] as string;
expect(msg).toContain('Belt computation failed');
},
);

Then(
'a warning is logged about agent confidence failure',
(world: CooldownBeltComputerWorld) => {
expect(world.loggerWarnSpy).toHaveBeenCalled();
const msg = world.loggerWarnSpy.mock.calls[0]![0] as string;
expect(msg).toContain('Agent confidence computation failed');
},
);

// 'cooldown continues normally' step is shared β€” defined in bridge-run-syncer.steps.ts
Loading
Loading