Skip to content

Commit 0f3c451

Browse files
authored
fix: address #3045 #3064 #3065 + new ruflo-deepseek-harness plugin (3.38.13) (#3078)
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 fa13ee4. 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
1 parent df0c982 commit 0f3c451

14 files changed

Lines changed: 644 additions & 6 deletions

File tree

.claude/helpers/statusline.cjs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,47 @@ function readJSON(filePath) {
630630

631631
// ─── Git info (pure-Node / single exec — needed for branch display) ──────────
632632

633+
// #3045 — getGitInfo() shells out to a 5-command chain per render. Uncached,
634+
// it queues subprocesses faster than they finish on large repos with multiple
635+
// concurrent Claude Code sessions, producing the pileup reported in that
636+
// issue (hundreds of orphan git children, load-avg in the hundreds). Cache
637+
// the parsed result in a per-cwd tmp file with a short TTL — dirty/branch
638+
// status doesn't need per-render freshness, and 5s is short enough to feel
639+
// live.
640+
const GIT_INFO_CACHE_FILE = path.join(
641+
os.tmpdir(),
642+
'ruflo-statusline-gitinfo-' +
643+
require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) +
644+
'.json'
645+
);
646+
const GIT_INFO_TTL_MS = 5000;
647+
648+
function readGitInfoCache() {
649+
try {
650+
if (fs.existsSync(GIT_INFO_CACHE_FILE)) {
651+
const raw = JSON.parse(fs.readFileSync(GIT_INFO_CACHE_FILE, 'utf-8'));
652+
if (raw && typeof raw._ts === 'number' && Date.now() - raw._ts < GIT_INFO_TTL_MS && raw.data) {
653+
return raw.data;
654+
}
655+
}
656+
} catch { /* ignore */ }
657+
return null;
658+
}
659+
660+
function writeGitInfoCache(data) {
661+
try {
662+
fs.writeFileSync(
663+
GIT_INFO_CACHE_FILE,
664+
JSON.stringify({ _ts: Date.now(), data }),
665+
'utf-8'
666+
);
667+
} catch { /* ignore */ }
668+
}
669+
633670
function getGitInfo() {
671+
const cached = readGitInfoCache();
672+
if (cached) return cached;
673+
634674
const result = {
635675
name: path.basename(CWD) || 'project', gitBranch: '', modified: 0, untracked: 0,
636676
staged: 0, ahead: 0, behind: 0,
@@ -649,7 +689,12 @@ function getGitInfo() {
649689
].join('; ');
650690

651691
const raw = safeExec("sh -c '" + script + "'", 3000);
652-
if (!raw) return result;
692+
if (!raw) {
693+
// Cache even the empty result — if git is slow/unavailable, we don't want
694+
// to re-attempt on every render either.
695+
writeGitInfoCache(result);
696+
return result;
697+
}
653698

654699
const parts = raw.split('---SEP---').map(function(s) { return s.trim(); });
655700
if (parts.length >= 5) {
@@ -673,6 +718,7 @@ function getGitInfo() {
673718
result.behind = parseInt(ab[1]) || 0;
674719
}
675720

721+
writeGitInfoCache(result);
676722
return result;
677723
}
678724

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-flow",
3-
"version": "3.38.12",
3+
"version": "3.38.13",
44
"workspaces": [
55
"v3/@claude-flow/codex",
66
"v3/@claude-flow/plugin-agent-federation",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
name: deepseek-architect
3+
description: DeepSeek harness architect for ruflo. Surfaces DeepSeek's chat and reasoning models via skills; enforces the ADR-150 removability contract (this plugin as optional augmentation, never a required runtime dep); routes between deepseek-chat and deepseek-reasoner based on task shape
4+
model: haiku
5+
---
6+
7+
You are the deepseek-architect for ruflo. Your job is to expose the
8+
DeepSeek API (`deepseek-chat`, `deepseek-reasoner`) through ruflo's UX
9+
while keeping ruflo independently operational at all times.
10+
11+
## ADR-150 invariants (load-bearing)
12+
13+
1. **Removable** — deleting `plugins/ruflo-deepseek-harness/` must not
14+
break any other ruflo functionality.
15+
2. **No hard dependency** — nothing in this plugin gets added to ruflo's
16+
`dependencies` in `package.json`. Scripts use `fetch` (Node 18+) and
17+
Node built-ins only.
18+
3. **Graceful degradation** — every script exits 0 with a
19+
`{ status: 'degraded'|'error', reason, hint? }` JSON envelope when
20+
`DEEPSEEK_API_KEY` is unset or the API is unreachable. The
21+
`emitAndExit(...)` helper in `scripts/_deepseek.mjs` is the reference
22+
implementation. Pass `--alert-on-error` to opt into hard failure for
23+
CI gates.
24+
4. **No secret in logs** — the API key is only read from
25+
`process.env.DEEPSEEK_API_KEY` and sent as a Bearer header. It is
26+
never printed to stdout/stderr.
27+
28+
If a PR breaks any of these four rules, it is a breaking change and
29+
needs its own ADR.
30+
31+
## Skills
32+
33+
| Skill | Role | Invoke when |
34+
|-------|------|-------------|
35+
| `deepseek-chat` | Non-reasoning single-turn completion via `deepseek-chat` | Summarization, extraction, quick classification, cheap Q&A |
36+
| `deepseek-reason` | Reasoning-mode completion via `deepseek-reasoner` (surfaces the CoT) | Proofs, plans, root-cause analysis, audits that need explicit reasoning |
37+
38+
## Routing heuristic
39+
40+
- Default to `deepseek-chat` for anything a smaller model can plausibly
41+
do in one turn.
42+
- Escalate to `deepseek-reason` when the task calls for multi-step
43+
reasoning AND the caller either wants to see the chain-of-thought or
44+
is willing to pay the higher token cost for the quality lift.
45+
- If the caller wants the CoT displayed, use `deepseek-reason
46+
--show-reasoning` in table mode; for programmatic consumption, use
47+
JSON mode which always includes `reasoning` and a `reasoningTokens`
48+
breakdown.
49+
50+
## Extending the plugin
51+
52+
Add a new skill by:
53+
54+
1. Creating `skills/<skill-name>/SKILL.md` with YAML frontmatter (name,
55+
description in **quotes** — see #3065 for why unquoted colons break
56+
`npx skills add`).
57+
2. Adding a matching `scripts/<name>.mjs` that imports
58+
`deepseekChat` / `parseArgs` / `emitAndExit` from `_deepseek.mjs` so
59+
it inherits the graceful-degradation contract for free.
60+
3. Documenting the new subcommand in
61+
`commands/ruflo-deepseek-harness.md` so the top-level command
62+
dispatcher lists it.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
name: ruflo-deepseek-harness
3+
description: DeepSeek harness integration — chat and reasoning-mode completions via the OpenAI-compatible DeepSeek API, wrapped as ruflo skills with graceful degradation (ADR-150 pattern)
4+
---
5+
6+
DeepSeek harness commands. All shell out to
7+
[`scripts/_deepseek.mjs`](../scripts/_deepseek.mjs) which hits DeepSeek's
8+
`https://api.deepseek.com/v1/chat/completions` endpoint using
9+
`DEEPSEEK_API_KEY` from the environment. The plugin never becomes a hard
10+
runtime dependency of ruflo — with the key unset or the API unreachable
11+
every script exits 0 with a `{ status: 'degraded', reason }` envelope,
12+
matching the ADR-150 removability contract used by the sibling
13+
`ruflo-metaharness` plugin.
14+
15+
**`deepseek chat --prompt <text> [--system <text>] [--model deepseek-chat] [--temperature 0.7] [--max-tokens 1024] [--format table|json] [--alert-on-error]`** — one-shot completion against the non-reasoning `deepseek-chat` model.
16+
1. Run `node plugins/ruflo-deepseek-harness/scripts/chat.mjs --prompt "..."`
17+
2. Emits `{ status, model, content, finishReason, usage }` as JSON (default) or the bare content (`--format table`)
18+
3. `--alert-on-error` exits 1 on any degraded/error status — CI-friendly gate
19+
4. See [`skills/deepseek-chat/SKILL.md`](../skills/deepseek-chat/SKILL.md)
20+
21+
**`deepseek reason --prompt <text> [--system <text>] [--model deepseek-reasoner] [--max-tokens 4096] [--show-reasoning] [--format table|json] [--alert-on-error]`** — reasoning-mode completion; separates `reasoning_content` (chain of thought) from `content` (final answer).
22+
1. Run `node plugins/ruflo-deepseek-harness/scripts/reason.mjs --prompt "..."`
23+
2. `temperature`/`top_p` are NOT forwarded — DeepSeek ignores them for reasoner models
24+
3. JSON output always includes `reasoning` + a `reasoningTokens` cost breakdown; table mode omits reasoning unless `--show-reasoning` is passed
25+
4. See [`skills/deepseek-reason/SKILL.md`](../skills/deepseek-reason/SKILL.md)
26+
27+
## Environment
28+
29+
- `DEEPSEEK_API_KEY` — required to actually call the API. Without it every
30+
script exits 0 with `{ status: 'degraded', reason: 'DEEPSEEK_API_KEY is not set' }`.
31+
Get a key at https://platform.deepseek.com.
32+
33+
## ADR-150 invariants (same as ruflo-metaharness)
34+
35+
1. **Removable**`rm -rf plugins/ruflo-deepseek-harness/` must leave ruflo working.
36+
2. **No hard dependency** — nothing in ruflo's `dependencies`. This plugin's
37+
scripts import only Node built-ins + `fetch` (Node 18+).
38+
3. **Graceful degradation** — every script exits 0 with a JSON envelope on
39+
failure. Pass `--alert-on-error` to opt into hard failure for CI gates.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Shared helper for the ruflo-deepseek-harness plugin.
4+
*
5+
* Calls the DeepSeek OpenAI-compatible Chat Completions API
6+
* (https://api.deepseek.com/v1/chat/completions). Reads the key from
7+
* DEEPSEEK_API_KEY. Never crashes ruflo on boot — every failure emits a
8+
* JSON object with { status: 'degraded', reason } and exits 0 by default;
9+
* pass --alert-on-error to exit 1 on a hard failure (missing key, HTTP
10+
* error, timeout).
11+
*
12+
* Modeled on plugins/ruflo-metaharness/scripts/_harness.mjs' graceful
13+
* degradation shape (ADR-150), so this plugin never becomes a hard runtime
14+
* dependency of ruflo — remove it and everything else keeps working.
15+
*/
16+
17+
const API_URL = 'https://api.deepseek.com/v1/chat/completions';
18+
const DEFAULT_TIMEOUT_MS = 60_000;
19+
20+
/**
21+
* Parse a minimal --key value CLI. Booleans: --flag with no value.
22+
*/
23+
export function parseArgs(argv) {
24+
const out = {};
25+
for (let i = 0; i < argv.length; i++) {
26+
const tok = argv[i];
27+
if (!tok.startsWith('--')) continue;
28+
const key = tok.slice(2);
29+
const next = argv[i + 1];
30+
if (next && !next.startsWith('--')) {
31+
out[key] = next;
32+
i++;
33+
} else {
34+
out[key] = true;
35+
}
36+
}
37+
return out;
38+
}
39+
40+
/**
41+
* Emit a degraded/OK JSON envelope and exit.
42+
* status: 'ok' | 'degraded' | 'error'
43+
*/
44+
export function emitAndExit(payload, { alertOnError = false } = {}) {
45+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
46+
const isFailure = payload.status === 'degraded' || payload.status === 'error';
47+
process.exit(isFailure && alertOnError ? 1 : 0);
48+
}
49+
50+
/**
51+
* Call DeepSeek's chat/completions endpoint.
52+
*
53+
* @param {object} opts
54+
* @param {string} opts.model e.g. "deepseek-chat", "deepseek-reasoner"
55+
* @param {Array} opts.messages OpenAI-style messages
56+
* @param {number} [opts.temperature]
57+
* @param {number} [opts.maxTokens]
58+
* @param {number} [opts.timeoutMs]
59+
* @returns {Promise<{ok: true, data: object} | {ok: false, reason: string, hint?: string}>}
60+
*/
61+
export async function deepseekChat({ model, messages, temperature, maxTokens, timeoutMs }) {
62+
const apiKey = process.env.DEEPSEEK_API_KEY;
63+
if (!apiKey) {
64+
return {
65+
ok: false,
66+
reason: 'DEEPSEEK_API_KEY is not set',
67+
hint: 'Get a key at https://platform.deepseek.com and export DEEPSEEK_API_KEY=... in your shell.',
68+
};
69+
}
70+
71+
const controller = new AbortController();
72+
const t = setTimeout(() => controller.abort(), timeoutMs || DEFAULT_TIMEOUT_MS);
73+
74+
try {
75+
const body = { model, messages };
76+
if (typeof temperature === 'number') body.temperature = temperature;
77+
if (typeof maxTokens === 'number') body.max_tokens = maxTokens;
78+
79+
const res = await fetch(API_URL, {
80+
method: 'POST',
81+
headers: {
82+
'content-type': 'application/json',
83+
authorization: 'Bearer ' + apiKey,
84+
},
85+
body: JSON.stringify(body),
86+
signal: controller.signal,
87+
});
88+
89+
if (!res.ok) {
90+
const text = await res.text().catch(() => '');
91+
return {
92+
ok: false,
93+
reason: 'HTTP ' + res.status + ' from DeepSeek',
94+
hint: text.slice(0, 500),
95+
};
96+
}
97+
98+
const data = await res.json();
99+
return { ok: true, data };
100+
} catch (err) {
101+
if (err && err.name === 'AbortError') {
102+
return { ok: false, reason: 'DeepSeek call timed out after ' + (timeoutMs || DEFAULT_TIMEOUT_MS) + 'ms' };
103+
}
104+
return { ok: false, reason: err && err.message ? err.message : String(err) };
105+
} finally {
106+
clearTimeout(t);
107+
}
108+
}
109+
110+
/**
111+
* Convenience: pull the first choice's message content (and reasoning_content
112+
* when present, for deepseek-reasoner).
113+
*/
114+
export function extractCompletion(data) {
115+
const choice = data && data.choices && data.choices[0];
116+
if (!choice || !choice.message) return { content: '', reasoning: '' };
117+
return {
118+
content: choice.message.content || '',
119+
reasoning: choice.message.reasoning_content || '',
120+
finishReason: choice.finish_reason || null,
121+
};
122+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env node
2+
/**
3+
* DeepSeek chat completion — non-reasoning model (`deepseek-chat`).
4+
*
5+
* Usage:
6+
* node plugins/ruflo-deepseek-harness/scripts/chat.mjs \
7+
* --prompt "Summarize the following in one sentence: ..." \
8+
* [--system "You are a concise assistant."] \
9+
* [--model deepseek-chat] \
10+
* [--temperature 0.7] \
11+
* [--max-tokens 1024] \
12+
* [--format table|json] \
13+
* [--alert-on-error]
14+
*/
15+
16+
import { parseArgs, emitAndExit, deepseekChat, extractCompletion } from './_deepseek.mjs';
17+
18+
const args = parseArgs(process.argv.slice(2));
19+
const alertOnError = args['alert-on-error'] === true;
20+
21+
const prompt = args.prompt;
22+
if (!prompt || prompt === true) {
23+
emitAndExit(
24+
{ status: 'error', reason: '--prompt is required (a string of at least 1 char)' },
25+
{ alertOnError: true } // hard-error even without the flag: this is a usage bug
26+
);
27+
}
28+
29+
const messages = [];
30+
if (args.system && args.system !== true) messages.push({ role: 'system', content: args.system });
31+
messages.push({ role: 'user', content: prompt });
32+
33+
const model = (args.model && args.model !== true) ? args.model : 'deepseek-chat';
34+
const temperature = args.temperature !== undefined ? Number(args.temperature) : undefined;
35+
const maxTokens = args['max-tokens'] !== undefined ? Number(args['max-tokens']) : undefined;
36+
37+
const result = await deepseekChat({ model, messages, temperature, maxTokens });
38+
39+
if (!result.ok) {
40+
emitAndExit(
41+
{ status: 'degraded', model, reason: result.reason, hint: result.hint || null },
42+
{ alertOnError }
43+
);
44+
}
45+
46+
const { content, finishReason } = extractCompletion(result.data);
47+
const usage = result.data.usage || {};
48+
49+
const payload = {
50+
status: 'ok',
51+
model,
52+
content,
53+
finishReason,
54+
usage: {
55+
promptTokens: usage.prompt_tokens ?? null,
56+
completionTokens: usage.completion_tokens ?? null,
57+
totalTokens: usage.total_tokens ?? null,
58+
},
59+
};
60+
61+
if (args.format === 'table') {
62+
process.stdout.write(content + '\n');
63+
process.exit(0);
64+
}
65+
66+
emitAndExit(payload);

0 commit comments

Comments
 (0)