mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
0f3c45101b
Bug fixes
- #3045 statusline.cjs: getGitInfo() runs the 5-command git chain per render;
on large repos with concurrent Claude Code sessions this queued subprocesses
faster than they finished (reporter observed hundreds of orphan children +
load-avg in the hundreds). Added a per-cwd tmp file cache with 5s TTL —
dirty status still feels live, pileup is bounded.
- #3064 hooks post-task: the narrow ad-hoc regex /^[a-zA-Z0-9_-]+$/ silently
dropped every colon-namespaced plugin agent (ruflo-core:reviewer,
feature-dev:code-explorer, ...) — i.e. every Claude Code plugin agent. The
canonical validateIdentifier() upstream already allows ':' and '.', so the
redundant regex is removed. Regression test locks in 4 agent-shape cases,
proven to catch the bug: 2 pass / 2 fail on revert, 4 pass with fix.
- #3065 harness-gepa SKILL.md: unquoted colon in `description` broke YAML
parsing in `npx skills add`. Quoted the description; rephrased the bare
"(default:" to avoid the leading colon.
New plugin: ruflo-deepseek-harness
- plugins/ruflo-deepseek-harness/: sibling to ruflo-metaharness (ADR-150
shape). Two skills: `deepseek-chat` (non-reasoning) and `deepseek-reason`
(surfaces reasoning_content separately). Reads DEEPSEEK_API_KEY from env;
degrades gracefully (exit 0 with `{status: 'degraded', reason, hint}`)
when the key is missing or the API is unreachable. `--alert-on-error`
flag opts into hard exit 1 for CI gates. Smoke-tested locally.
Release
- Bump @claude-flow/cli, claude-flow, ruflo: 3.38.12 → 3.38.13 (PATCH:
bug fixes; the new plugin is scaffolding under plugins/ and not part of
the npm-published CLI packages).
Not fixed
- #3051 memory_store tags: current source's memory_store handler passes
tags straight through to storeEntry; a maintainer already verified live
round-trip works on fa13ee4ad. Reporter has not yet supplied the
ruflo/@claude-flow/cli version that hosted the affected MCP server,
so the affected release path is unknown. Left as-is pending that reply.
Claude-Session: https://claude.ai/code/session_0118jMsYhwHD5dx2vStENsEB
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
/**
|
|
* Regression guard for #3064 — `hooks_post-task` silently dropped the
|
|
* routing-outcome write when `--agent` contained a colon.
|
|
*
|
|
* The narrow ad-hoc regex `/^[a-zA-Z0-9_-]+$/` at the write gate rejected
|
|
* every Claude Code plugin agent (which are always namespaced
|
|
* `plugin:agent` — e.g. `ruflo-core:reviewer`, `feature-dev:code-explorer`),
|
|
* so any project driving `post-task` from a `PostToolUse:Task` hook fed
|
|
* the router zero usable signal from plugin agents.
|
|
*
|
|
* The canonical `validateIdentifier()` check earlier in the handler
|
|
* already allows `:` and `.` (IDENTIFIER_RE in validate-input.ts), so the
|
|
* redundant narrow regex was simply removed. This test locks in that
|
|
* colon-namespaced agents now produce a routing-outcomes.json append.
|
|
*
|
|
* NOTE: hooks-tools.ts captures `ROUTING_OUTCOMES_PATH = resolve('.') + ...`
|
|
* at module load, so the chdir MUST happen before the dynamic import.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
// Move cwd into a temp dir BEFORE the dynamic import below runs — otherwise
|
|
// the routing-outcomes file gets written into whatever cwd vitest ran from.
|
|
const testRoot = mkdtempSync(join(tmpdir(), 'ruflo-3064-'));
|
|
const origCwd = process.cwd();
|
|
process.chdir(testRoot);
|
|
|
|
const bridgeRecordFeedback = vi.fn(async () => ({ success: true, controller: 'mock', updated: 1 }));
|
|
const bridgeRecordCausalEdge = vi.fn(async () => ({ success: true, controller: 'mock' }));
|
|
const bridgeStoreEntry = vi.fn(async () => ({ success: true, controller: 'mock' }));
|
|
|
|
vi.mock('../src/memory/memory-bridge.js', () => ({
|
|
bridgeRecordFeedback,
|
|
bridgeRecordCausalEdge,
|
|
bridgeStoreEntry,
|
|
}));
|
|
|
|
vi.mock('../src/memory/intelligence.js', () => ({
|
|
recordTrajectory: vi.fn(async () => undefined),
|
|
}));
|
|
vi.mock('../src/memory/graph-edge-writer.js', () => ({
|
|
insertGraphEdge: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
const { hooksPostTask } = await import('../src/mcp-tools/hooks-tools.js');
|
|
|
|
const outcomesPath = join(testRoot, '.claude-flow', 'routing-outcomes.json');
|
|
|
|
function readOutcomes(): Array<{ task: string; agent: string; success: boolean }> {
|
|
if (!existsSync(outcomesPath)) return [];
|
|
const raw = JSON.parse(readFileSync(outcomesPath, 'utf-8'));
|
|
if (Array.isArray(raw)) return raw;
|
|
if (raw && Array.isArray(raw.outcomes)) return raw.outcomes;
|
|
return [];
|
|
}
|
|
|
|
beforeAll(() => {
|
|
// Restore cwd for the rest of the test runner — the module has already
|
|
// captured its own path, so it doesn't matter what cwd is now.
|
|
process.chdir(origCwd);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Fresh outcomes file per test so counts start at 0.
|
|
mkdirSync(join(testRoot, '.claude-flow'), { recursive: true });
|
|
writeFileSync(outcomesPath, JSON.stringify({ outcomes: [] }));
|
|
});
|
|
|
|
afterAll(() => {
|
|
try { rmSync(testRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
});
|
|
|
|
describe('#3064 — hooks_post-task accepts colon-namespaced plugin agents', () => {
|
|
const cases = [
|
|
{ label: 'plain identifier (baseline)', agent: 'reviewer' },
|
|
{ label: 'hyphenated identifier (baseline)', agent: 'ruflo-core-reviewer' },
|
|
{ label: 'colon-namespaced plugin agent (bug case)', agent: 'ruflo-core:reviewer' },
|
|
{ label: 'nested colon-namespaced plugin agent', agent: 'feature-dev:code-explorer' },
|
|
];
|
|
|
|
for (const { label, agent } of cases) {
|
|
it(`records the routing outcome for ${label}: "${agent}"`, async () => {
|
|
const taskId = `probe-${agent.replace(/[^a-z0-9]/gi, '_')}`;
|
|
|
|
await hooksPostTask.handler({
|
|
taskId,
|
|
task: `test task for ${agent}`,
|
|
agent,
|
|
success: true,
|
|
quality: 0.85,
|
|
});
|
|
|
|
const outcomes = readOutcomes();
|
|
expect(outcomes.length).toBe(1);
|
|
|
|
const [only] = outcomes;
|
|
expect(only.agent).toBe(agent);
|
|
expect(only.success).toBe(true);
|
|
expect(only.task).toBe(`test task for ${agent}`);
|
|
});
|
|
}
|
|
});
|