@claude-flow/cli 3.32.34 → 3.32.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +4 -4
  3. package/dist/src/commands/agent.js +49 -1
  4. package/dist/src/commands/hooks.js +24 -0
  5. package/dist/src/commands/metaharness.js +4 -0
  6. package/dist/src/commands/swarm.js +61 -5
  7. package/dist/src/config-adapter.js +1 -0
  8. package/dist/src/index.js +3 -2
  9. package/dist/src/mcp-tools/agent-tools.js +10 -0
  10. package/dist/src/mcp-tools/hooks-tools.js +86 -52
  11. package/dist/src/mcp-tools/memory-tools.js +14 -2
  12. package/dist/src/mcp-tools/metaharness-tools.js +7 -0
  13. package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
  14. package/dist/src/mcp-tools/swarm-tools.js +172 -7
  15. package/dist/src/memory/memory-initializer.d.ts +9 -1
  16. package/dist/src/memory/memory-initializer.js +66 -19
  17. package/dist/src/services/daemon-autostart.js +7 -4
  18. package/dist/src/services/flywheel-receipt.d.ts +3 -0
  19. package/dist/src/services/flywheel-receipt.js +1 -0
  20. package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
  21. package/dist/src/services/harness-flywheel-runtime.js +19 -19
  22. package/dist/src/services/harness-flywheel.d.ts +2 -0
  23. package/dist/src/services/harness-flywheel.js +1 -0
  24. package/dist/src/services/harness-project-anchor.d.ts +23 -0
  25. package/dist/src/services/harness-project-anchor.js +129 -0
  26. package/dist/src/services/learned-routing.d.ts +34 -0
  27. package/dist/src/services/learned-routing.js +85 -0
  28. package/dist/src/services/pheromone-adaptive.d.ts +71 -0
  29. package/dist/src/services/pheromone-adaptive.js +214 -0
  30. package/dist/src/services/worker-daemon.js +4 -1
  31. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Project-local, hash-pinned flywheel anchors (#2840).
3
+ *
4
+ * Downstream repositories must be evaluated against their own labelled
5
+ * retrieval tasks. Silently using Ruflo's development-history benchmark makes
6
+ * the objective flat and indistinguishable from "already optimal", so foreign
7
+ * projects fail closed unless they supply a pinned anchor manifest.
8
+ */
9
+ import { existsSync, readFileSync, realpathSync, } from 'node:fs';
10
+ import { dirname, isAbsolute, join, relative, resolve, sep, } from 'node:path';
11
+ import { FROZEN_HUMAN_EVAL_HASH, loadFrozenHumanEval, humanEvalHash, } from './harness-frozen-eval.js';
12
+ export const PROJECT_ANCHOR_SCHEMA = 'ruflo.flywheel-anchor/v1';
13
+ export const PROJECT_ANCHOR_MANIFEST_SCHEMA = 'ruflo.flywheel-anchor-manifest/v1';
14
+ export const DEFAULT_PROJECT_ANCHOR_MANIFEST = join('.claude', 'eval', 'flywheel-anchor.manifest.json');
15
+ function normalizeHash(value) {
16
+ const trimmed = value.trim().toLowerCase();
17
+ return trimmed.startsWith('sha256:') ? trimmed : `sha256:${trimmed}`;
18
+ }
19
+ function containedPath(projectRoot, requested) {
20
+ const root = realpathSync(resolve(projectRoot));
21
+ const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
22
+ const lexical = relative(root, absolute);
23
+ if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
24
+ throw new Error('flywheel anchor path must stay inside project root');
25
+ }
26
+ const actual = realpathSync(absolute);
27
+ const physical = relative(root, actual);
28
+ if (physical === '..' || physical.startsWith(`..${sep}`) || isAbsolute(physical)) {
29
+ throw new Error('flywheel anchor symlink escapes project root');
30
+ }
31
+ return actual;
32
+ }
33
+ function parseTasks(path) {
34
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
35
+ if (parsed.schemaVersion && parsed.schemaVersion !== PROJECT_ANCHOR_SCHEMA) {
36
+ throw new Error(`unsupported flywheel anchor schema: ${parsed.schemaVersion}`);
37
+ }
38
+ if (!Array.isArray(parsed.tasks) || parsed.tasks.length < 4) {
39
+ throw new Error('project flywheel anchor requires at least 4 labelled tasks');
40
+ }
41
+ const ids = new Set();
42
+ for (const [index, task] of parsed.tasks.entries()) {
43
+ if (!task || typeof task.id !== 'string' || !/^[A-Za-z0-9._-]{1,128}$/.test(task.id)) {
44
+ throw new Error(`invalid anchor task id at index ${index}`);
45
+ }
46
+ if (ids.has(task.id))
47
+ throw new Error(`duplicate anchor task id: ${task.id}`);
48
+ ids.add(task.id);
49
+ if (typeof task.q !== 'string' || task.q.trim().length === 0) {
50
+ throw new Error(`anchor task ${task.id} has no query`);
51
+ }
52
+ if (!Array.isArray(task.labels) || task.labels.length === 0 || task.labels.some((label) => typeof label !== 'string' || !label.trim())) {
53
+ throw new Error(`anchor task ${task.id} requires non-empty string labels`);
54
+ }
55
+ }
56
+ return { version: parsed.version ?? 'project-anchor-v1', tasks: parsed.tasks };
57
+ }
58
+ function toSelection(path, expectedHash) {
59
+ const parsed = parseTasks(path);
60
+ const actualHash = humanEvalHash(parsed.tasks);
61
+ if (actualHash !== normalizeHash(expectedHash)) {
62
+ throw new Error(`project flywheel anchor hash mismatch (got ${actualHash}, pinned ${normalizeHash(expectedHash)})`);
63
+ }
64
+ return {
65
+ version: parsed.version,
66
+ anchorRef: actualHash,
67
+ source: 'project',
68
+ path,
69
+ tasks: parsed.tasks.map((task) => ({
70
+ id: task.id,
71
+ input: { id: task.id, q: task.q },
72
+ expected: task.labels,
73
+ })),
74
+ };
75
+ }
76
+ function isRufloRepository(projectRoot) {
77
+ try {
78
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
79
+ const repository = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
80
+ return ['claude-flow', 'ruflo', '@claude-flow/cli'].includes(pkg.name ?? '')
81
+ && /github\.com[/:]ruvnet\/(?:ruflo|claude-flow)(?:\.git)?$/i.test(repository ?? '');
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ export function loadEffectiveFlywheelAnchor(projectRoot, options = {}) {
88
+ const root = resolve(projectRoot);
89
+ if (!!options.anchorPath !== !!options.anchorHash) {
90
+ throw new Error('anchorPath and anchorHash must be supplied together');
91
+ }
92
+ if (options.anchorPath && options.anchorHash) {
93
+ return toSelection(containedPath(root, options.anchorPath), options.anchorHash);
94
+ }
95
+ const manifestCandidate = options.manifestPath ?? DEFAULT_PROJECT_ANCHOR_MANIFEST;
96
+ const manifestPath = isAbsolute(manifestCandidate)
97
+ ? manifestCandidate
98
+ : resolve(root, manifestCandidate);
99
+ if (existsSync(manifestPath)) {
100
+ const containedManifest = containedPath(root, manifestPath);
101
+ const manifest = JSON.parse(readFileSync(containedManifest, 'utf8'));
102
+ if (manifest.schemaVersion !== PROJECT_ANCHOR_MANIFEST_SCHEMA) {
103
+ throw new Error(`unsupported flywheel anchor manifest schema: ${manifest.schemaVersion}`);
104
+ }
105
+ if (typeof manifest.path !== 'string' || typeof manifest.sha256 !== 'string') {
106
+ throw new Error('flywheel anchor manifest requires path and sha256');
107
+ }
108
+ // Manifest-relative paths are easier to relocate while remaining
109
+ // repository-contained; project-relative paths remain supported.
110
+ const manifestRelative = resolve(dirname(containedManifest), manifest.path);
111
+ const requested = existsSync(manifestRelative) ? manifestRelative : resolve(root, manifest.path);
112
+ return toSelection(containedPath(root, requested), manifest.sha256);
113
+ }
114
+ if (isRufloRepository(root) || process.env.RUFLO_FLYWHEEL_ALLOW_BUILTIN_ANCHOR === '1') {
115
+ const frozen = loadFrozenHumanEval();
116
+ return {
117
+ version: frozen.version,
118
+ anchorRef: FROZEN_HUMAN_EVAL_HASH,
119
+ source: 'ruflo-built-in',
120
+ tasks: frozen.tasks.map((task) => ({
121
+ id: task.id,
122
+ input: { id: task.id, q: task.q },
123
+ expected: task.labels,
124
+ })),
125
+ };
126
+ }
127
+ throw new Error(`project-local flywheel anchor required; create ${DEFAULT_PROJECT_ANCHOR_MANIFEST} or pass anchorPath + anchorHash`);
128
+ }
129
+ //# sourceMappingURL=harness-project-anchor.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Discriminative routing-pattern learner (#2839).
3
+ *
4
+ * Outcomes are labelled by the agent that actually completed the task. The
5
+ * previous implementation collapsed every successful task for an agent into
6
+ * one insertion-ordered Set and kept the first 50 words forever. This module
7
+ * ranks terms by within-agent support, cross-agent rarity, outcome quality,
8
+ * and discriminative share. Generic terms therefore lose to stable,
9
+ * agent-specific evidence, and later outcomes can replace earlier noise.
10
+ */
11
+ export interface LearnedRoutingOutcome {
12
+ task: string;
13
+ agent: string;
14
+ success: boolean;
15
+ quality: number;
16
+ keywords: string[];
17
+ timestamp: string;
18
+ }
19
+ export interface LearnedRoutingPattern {
20
+ keywords: string[];
21
+ agents: string[];
22
+ source: 'learned';
23
+ support: number;
24
+ reliability: number;
25
+ }
26
+ export interface LearnedRoutingOptions {
27
+ maxKeywords?: number;
28
+ minAgentOutcomes?: number;
29
+ minKeywordSupport?: number;
30
+ minOutcomeQuality?: number;
31
+ minDiscriminativeShare?: number;
32
+ }
33
+ export declare function buildLearnedRoutingPatterns(outcomes: LearnedRoutingOutcome[], options?: LearnedRoutingOptions): Record<string, LearnedRoutingPattern>;
34
+ //# sourceMappingURL=learned-routing.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Discriminative routing-pattern learner (#2839).
3
+ *
4
+ * Outcomes are labelled by the agent that actually completed the task. The
5
+ * previous implementation collapsed every successful task for an agent into
6
+ * one insertion-ordered Set and kept the first 50 words forever. This module
7
+ * ranks terms by within-agent support, cross-agent rarity, outcome quality,
8
+ * and discriminative share. Generic terms therefore lose to stable,
9
+ * agent-specific evidence, and later outcomes can replace earlier noise.
10
+ */
11
+ const finiteUnit = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
12
+ export function buildLearnedRoutingPatterns(outcomes, options = {}) {
13
+ const maxKeywords = Math.max(1, Math.min(options.maxKeywords ?? 50, 100));
14
+ const minAgentOutcomes = Math.max(1, options.minAgentOutcomes ?? 2);
15
+ const minKeywordSupport = Math.max(1, options.minKeywordSupport ?? 2);
16
+ const minOutcomeQuality = finiteUnit(options.minOutcomeQuality ?? 0.65);
17
+ const minDiscriminativeShare = finiteUnit(options.minDiscriminativeShare ?? 0.6);
18
+ const accepted = outcomes.filter((outcome) => outcome.success
19
+ && typeof outcome.agent === 'string'
20
+ && outcome.agent.length > 0
21
+ && finiteUnit(outcome.quality) >= minOutcomeQuality
22
+ && Array.isArray(outcome.keywords)
23
+ && outcome.keywords.length > 0);
24
+ const agents = [...new Set(accepted.map((outcome) => outcome.agent))].sort();
25
+ if (agents.length === 0)
26
+ return {};
27
+ const agentOutcomes = new Map();
28
+ const evidence = new Map();
29
+ const globalSupport = new Map();
30
+ const agentDocumentFrequency = new Map();
31
+ for (const agent of agents) {
32
+ const rows = accepted.filter((outcome) => outcome.agent === agent);
33
+ agentOutcomes.set(agent, rows);
34
+ const perKeyword = new Map();
35
+ for (const row of rows) {
36
+ const unique = new Set(row.keywords.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean));
37
+ for (const keyword of unique) {
38
+ const current = perKeyword.get(keyword) ?? { support: 0, qualityTotal: 0 };
39
+ current.support++;
40
+ current.qualityTotal += finiteUnit(row.quality);
41
+ perKeyword.set(keyword, current);
42
+ globalSupport.set(keyword, (globalSupport.get(keyword) ?? 0) + 1);
43
+ }
44
+ }
45
+ evidence.set(agent, perKeyword);
46
+ for (const keyword of perKeyword.keys()) {
47
+ agentDocumentFrequency.set(keyword, (agentDocumentFrequency.get(keyword) ?? 0) + 1);
48
+ }
49
+ }
50
+ const patterns = {};
51
+ for (const agent of agents) {
52
+ const rows = agentOutcomes.get(agent) ?? [];
53
+ if (rows.length < minAgentOutcomes)
54
+ continue;
55
+ const ranked = [...(evidence.get(agent) ?? new Map()).entries()]
56
+ .filter(([, item]) => item.support >= minKeywordSupport)
57
+ .map(([keyword, item]) => {
58
+ const totalSupport = globalSupport.get(keyword) ?? item.support;
59
+ const discriminativeShare = item.support / Math.max(1, totalSupport);
60
+ const documentFrequency = agentDocumentFrequency.get(keyword) ?? 1;
61
+ const idf = Math.log(1 + agents.length / documentFrequency);
62
+ const withinAgentSupport = item.support / rows.length;
63
+ const meanQuality = item.qualityTotal / item.support;
64
+ return {
65
+ keyword,
66
+ discriminativeShare,
67
+ score: withinAgentSupport * meanQuality * idf * discriminativeShare,
68
+ };
69
+ })
70
+ .filter((item) => item.discriminativeShare >= minDiscriminativeShare)
71
+ .sort((a, b) => b.score - a.score || a.keyword.localeCompare(b.keyword))
72
+ .slice(0, maxKeywords);
73
+ if (ranked.length === 0)
74
+ continue;
75
+ patterns[`learned-${agent}`] = {
76
+ keywords: ranked.map((item) => item.keyword),
77
+ agents: [agent],
78
+ source: 'learned',
79
+ support: rows.length,
80
+ reliability: rows.reduce((sum, row) => sum + finiteUnit(row.quality), 0) / rows.length,
81
+ };
82
+ }
83
+ return patterns;
84
+ }
85
+ //# sourceMappingURL=learned-routing.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ADR-330 — Adaptive Pheromone Swarm Consensus.
3
+ *
4
+ * This is a scheduling eligibility layer, not an agent terminator. It learns a
5
+ * bounded EMA signal per agent, compares agents against role-local baselines,
6
+ * and may suspend them from future dispatch. Coordinators and other protected
7
+ * roles remain eligible, quorum is never crossed, pruning per round is capped,
8
+ * and exploration periodically reactivates a suspended agent.
9
+ */
10
+ export declare const APSC_SCHEMA = "ruflo.apsc-state/v1";
11
+ export interface ApscConfig {
12
+ alpha: number;
13
+ beta: number;
14
+ gamma: number;
15
+ emaDecay: number;
16
+ pruningFactor: number;
17
+ reactivationThreshold: number;
18
+ minActiveAgents: number;
19
+ minSamples: number;
20
+ maxSuspendFraction: number;
21
+ explorationRate: number;
22
+ dryRun: boolean;
23
+ protectedRoles: string[];
24
+ }
25
+ export interface ApscAgentState {
26
+ agentId: string;
27
+ role: string;
28
+ status: 'active' | 'suspended';
29
+ samples: number;
30
+ rawScore: number;
31
+ normalizedScore: number;
32
+ emaScore: number;
33
+ lastUpdatedAt: string;
34
+ lastDecision?: 'keep' | 'would-suspend' | 'suspend' | 'reactivate' | 'explore';
35
+ }
36
+ export interface ApscState {
37
+ schemaVersion: typeof APSC_SCHEMA;
38
+ config: ApscConfig;
39
+ round: number;
40
+ threshold: number;
41
+ roleBaselines: Record<string, number>;
42
+ agents: Record<string, ApscAgentState>;
43
+ }
44
+ export interface ApscSignal {
45
+ agentId: string;
46
+ role: string;
47
+ taskSuccess: number;
48
+ normalizedLatency: number;
49
+ consensusAlignment: number;
50
+ }
51
+ export interface ApscDecision {
52
+ agentId: string;
53
+ score: number;
54
+ normalizedScore: number;
55
+ emaScore: number;
56
+ threshold: number;
57
+ action: 'keep' | 'would-suspend' | 'suspend' | 'reactivate' | 'explore';
58
+ applied: boolean;
59
+ reason: string;
60
+ activeAgents: number;
61
+ suspendedAgents: number;
62
+ }
63
+ export declare const DEFAULT_APSC_CONFIG: ApscConfig;
64
+ export declare function normalizeApscConfig(input?: Partial<ApscConfig>): ApscConfig;
65
+ export declare function createApscState(config?: Partial<ApscConfig>): ApscState;
66
+ export declare function recordApscSignal(current: ApscState, signal: ApscSignal, now?: number): {
67
+ state: ApscState;
68
+ decision: ApscDecision;
69
+ };
70
+ export declare function isApscAgentEligible(state: ApscState | undefined, agentId: string): boolean;
71
+ //# sourceMappingURL=pheromone-adaptive.d.ts.map
@@ -0,0 +1,214 @@
1
+ /**
2
+ * ADR-330 — Adaptive Pheromone Swarm Consensus.
3
+ *
4
+ * This is a scheduling eligibility layer, not an agent terminator. It learns a
5
+ * bounded EMA signal per agent, compares agents against role-local baselines,
6
+ * and may suspend them from future dispatch. Coordinators and other protected
7
+ * roles remain eligible, quorum is never crossed, pruning per round is capped,
8
+ * and exploration periodically reactivates a suspended agent.
9
+ */
10
+ export const APSC_SCHEMA = 'ruflo.apsc-state/v1';
11
+ export const DEFAULT_APSC_CONFIG = {
12
+ alpha: 0.5,
13
+ beta: 0.2,
14
+ gamma: 0.3,
15
+ emaDecay: 0.85,
16
+ pruningFactor: 0.6,
17
+ reactivationThreshold: 0.75,
18
+ minActiveAgents: 3,
19
+ minSamples: 3,
20
+ maxSuspendFraction: 0.25,
21
+ explorationRate: 0.1,
22
+ dryRun: true,
23
+ protectedRoles: ['coordinator', 'queen', 'security-architect', 'security-auditor'],
24
+ };
25
+ const unit = (value, field) => {
26
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
27
+ throw new Error(`${field} must be a finite number in [0,1]`);
28
+ }
29
+ return value;
30
+ };
31
+ export function normalizeApscConfig(input = {}) {
32
+ const alpha = unit(input.alpha ?? DEFAULT_APSC_CONFIG.alpha, 'alpha');
33
+ const beta = unit(input.beta ?? DEFAULT_APSC_CONFIG.beta, 'beta');
34
+ const gamma = unit(input.gamma ?? DEFAULT_APSC_CONFIG.gamma, 'gamma');
35
+ const weight = alpha + beta + gamma;
36
+ if (weight <= 0)
37
+ throw new Error('at least one APSC score weight must be positive');
38
+ const minActiveAgents = input.minActiveAgents ?? DEFAULT_APSC_CONFIG.minActiveAgents;
39
+ const minSamples = input.minSamples ?? DEFAULT_APSC_CONFIG.minSamples;
40
+ if (!Number.isInteger(minActiveAgents) || minActiveAgents < 1 || minActiveAgents > 50) {
41
+ throw new Error('minActiveAgents must be an integer in [1,50]');
42
+ }
43
+ if (!Number.isInteger(minSamples) || minSamples < 1 || minSamples > 1000) {
44
+ throw new Error('minSamples must be an integer in [1,1000]');
45
+ }
46
+ const protectedRoles = [...new Set((input.protectedRoles ?? DEFAULT_APSC_CONFIG.protectedRoles)
47
+ .map((role) => role.trim().toLowerCase())
48
+ .filter(Boolean))].sort();
49
+ return {
50
+ alpha: alpha / weight,
51
+ beta: beta / weight,
52
+ gamma: gamma / weight,
53
+ emaDecay: unit(input.emaDecay ?? DEFAULT_APSC_CONFIG.emaDecay, 'emaDecay'),
54
+ pruningFactor: unit(input.pruningFactor ?? DEFAULT_APSC_CONFIG.pruningFactor, 'pruningFactor'),
55
+ reactivationThreshold: unit(input.reactivationThreshold ?? DEFAULT_APSC_CONFIG.reactivationThreshold, 'reactivationThreshold'),
56
+ minActiveAgents,
57
+ minSamples,
58
+ maxSuspendFraction: unit(input.maxSuspendFraction ?? DEFAULT_APSC_CONFIG.maxSuspendFraction, 'maxSuspendFraction'),
59
+ explorationRate: unit(input.explorationRate ?? DEFAULT_APSC_CONFIG.explorationRate, 'explorationRate'),
60
+ dryRun: input.dryRun ?? DEFAULT_APSC_CONFIG.dryRun,
61
+ protectedRoles,
62
+ };
63
+ }
64
+ export function createApscState(config = {}) {
65
+ return {
66
+ schemaVersion: APSC_SCHEMA,
67
+ config: normalizeApscConfig(config),
68
+ round: 0,
69
+ threshold: 0.5,
70
+ roleBaselines: {},
71
+ agents: {},
72
+ };
73
+ }
74
+ function counts(state) {
75
+ const values = Object.values(state.agents);
76
+ return {
77
+ activeAgents: values.filter((agent) => agent.status === 'active').length,
78
+ suspendedAgents: values.filter((agent) => agent.status === 'suspended').length,
79
+ };
80
+ }
81
+ function protectedRole(state, role) {
82
+ return state.config.protectedRoles.includes(role.toLowerCase());
83
+ }
84
+ export function recordApscSignal(current, signal, now = Date.now()) {
85
+ if (!/^[A-Za-z0-9._:-]{1,160}$/.test(signal.agentId))
86
+ throw new Error('invalid APSC agentId');
87
+ if (!/^[A-Za-z0-9._:-]{1,100}$/.test(signal.role))
88
+ throw new Error('invalid APSC role');
89
+ const config = normalizeApscConfig(current.config);
90
+ const state = {
91
+ ...current,
92
+ config,
93
+ round: current.round + 1,
94
+ roleBaselines: { ...current.roleBaselines },
95
+ agents: Object.fromEntries(Object.entries(current.agents).map(([id, agent]) => [id, { ...agent }])),
96
+ };
97
+ const success = unit(signal.taskSuccess, 'taskSuccess');
98
+ const latency = unit(signal.normalizedLatency, 'normalizedLatency');
99
+ const alignment = unit(signal.consensusAlignment, 'consensusAlignment');
100
+ const rawScore = config.alpha * success + config.beta * (1 - latency) + config.gamma * alignment;
101
+ const role = signal.role.toLowerCase();
102
+ // Compare against peers in the same role, excluding the current agent.
103
+ // Letting an agent update its own baseline before normalization caused a
104
+ // persistently weak agent to redefine "normal" downward and reactivate
105
+ // without any recovery.
106
+ const rolePeers = Object.values(state.agents)
107
+ .filter((item) => item.agentId !== signal.agentId && item.role === role && item.samples > 0);
108
+ const peerMean = rolePeers.length
109
+ ? rolePeers.reduce((sum, item) => sum + item.rawScore, 0) / rolePeers.length
110
+ : undefined;
111
+ const priorRoleBaseline = peerMean ?? state.roleBaselines[role] ?? rawScore;
112
+ // A common absolute scale unfairly prunes coordinators/specialists whose
113
+ // observable task signal differs by role. Center each score on its role EMA.
114
+ const normalizedScore = Math.max(0, Math.min(1, 0.5 + rawScore - priorRoleBaseline));
115
+ const previous = state.agents[signal.agentId];
116
+ const agent = {
117
+ agentId: signal.agentId,
118
+ role,
119
+ status: previous?.status ?? 'active',
120
+ samples: (previous?.samples ?? 0) + 1,
121
+ rawScore,
122
+ normalizedScore,
123
+ emaScore: previous
124
+ ? config.emaDecay * previous.emaScore + (1 - config.emaDecay) * normalizedScore
125
+ : normalizedScore,
126
+ lastUpdatedAt: new Date(now).toISOString(),
127
+ lastDecision: 'keep',
128
+ };
129
+ state.agents[signal.agentId] = agent;
130
+ const allRoleAgents = Object.values(state.agents).filter((item) => item.role === role);
131
+ const observedRoleMean = allRoleAgents.reduce((sum, item) => sum + item.rawScore, 0) / allRoleAgents.length;
132
+ state.roleBaselines[role] = config.emaDecay * priorRoleBaseline + (1 - config.emaDecay) * observedRoleMean;
133
+ const mature = Object.values(state.agents).filter((item) => item.samples >= config.minSamples);
134
+ const observedMean = mature.length
135
+ ? mature.reduce((sum, item) => sum + item.emaScore, 0) / mature.length
136
+ : state.threshold;
137
+ state.threshold = config.emaDecay * state.threshold + (1 - config.emaDecay) * observedMean;
138
+ let action = 'keep';
139
+ let applied = false;
140
+ let reason = agent.samples < config.minSamples
141
+ ? `warm-up ${agent.samples}/${config.minSamples}`
142
+ : 'within adaptive envelope';
143
+ if (agent.status === 'suspended' && agent.emaScore >= state.threshold * config.reactivationThreshold) {
144
+ action = 'reactivate';
145
+ reason = 'score recovered above reactivation threshold';
146
+ if (!config.dryRun) {
147
+ agent.status = 'active';
148
+ applied = true;
149
+ }
150
+ }
151
+ else if (agent.status === 'active'
152
+ && agent.samples >= config.minSamples
153
+ && !protectedRole(state, agent.role)
154
+ && agent.emaScore < state.threshold * config.pruningFactor) {
155
+ const before = counts(state);
156
+ const maxThisRound = Math.floor(before.activeAgents * config.maxSuspendFraction);
157
+ const quorumCapacity = Math.max(0, before.activeAgents - config.minActiveAgents);
158
+ if (maxThisRound < 1 || quorumCapacity < 1) {
159
+ reason = 'safety floor prevents suspension';
160
+ }
161
+ else {
162
+ action = config.dryRun ? 'would-suspend' : 'suspend';
163
+ reason = 'score below adaptive pruning threshold';
164
+ if (!config.dryRun) {
165
+ agent.status = 'suspended';
166
+ applied = true;
167
+ }
168
+ }
169
+ }
170
+ else if (protectedRole(state, agent.role)) {
171
+ reason = 'protected role remains eligible';
172
+ }
173
+ // Deterministic exploration: at a bounded cadence, reactivate the stalest
174
+ // suspended agent so a transient failure cannot permanently exclude it.
175
+ if (!config.dryRun && config.explorationRate > 0) {
176
+ const cadence = Math.max(1, Math.round(1 / config.explorationRate));
177
+ if (state.round % cadence === 0) {
178
+ const explorer = Object.values(state.agents)
179
+ .filter((item) => item.status === 'suspended')
180
+ .sort((a, b) => a.lastUpdatedAt.localeCompare(b.lastUpdatedAt) || a.agentId.localeCompare(b.agentId))[0];
181
+ if (explorer) {
182
+ explorer.status = 'active';
183
+ explorer.lastDecision = 'explore';
184
+ if (explorer.agentId === agent.agentId) {
185
+ action = 'explore';
186
+ applied = true;
187
+ reason = 'bounded exploration reactivation';
188
+ }
189
+ }
190
+ }
191
+ }
192
+ agent.lastDecision = action;
193
+ const after = counts(state);
194
+ return {
195
+ state,
196
+ decision: {
197
+ agentId: agent.agentId,
198
+ score: rawScore,
199
+ normalizedScore,
200
+ emaScore: agent.emaScore,
201
+ threshold: state.threshold,
202
+ action,
203
+ applied,
204
+ reason,
205
+ ...after,
206
+ },
207
+ };
208
+ }
209
+ export function isApscAgentEligible(state, agentId) {
210
+ if (!state || state.config.dryRun)
211
+ return true;
212
+ return state.agents[agentId]?.status !== 'suspended';
213
+ }
214
+ //# sourceMappingURL=pheromone-adaptive.js.map
@@ -70,7 +70,10 @@ const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
70
70
  // half a day; set RUFLO_DAEMON_TTL_SECS=0 (or `--ttl 0`) to opt out. Idle
71
71
  // shutdown is opt-in (0 = disabled) since a legitimately quiet daemon is not a leak.
72
72
  const DEFAULT_DAEMON_TTL_MS = 12 * 60 * 60 * 1000;
73
- const DEFAULT_DAEMON_IDLE_SHUTDOWN_MS = 0;
73
+ // #2834 an auto-started daemon must not remain alive for the full 12h TTL
74
+ // after its workspace goes idle. Explicit 0 via config/env still disables the
75
+ // idle cap for operators who intentionally want a resident daemon.
76
+ const DEFAULT_DAEMON_IDLE_SHUTDOWN_MS = 30 * 60 * 1000;
74
77
  /**
75
78
  * Read a non-negative seconds value from an env var and return it as ms.
76
79
  * Unlike the `parseInt(x) || default` idiom used elsewhere, an explicit `0`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.32.34",
3
+ "version": "3.32.35",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",