@claude-flow/cli 3.55.0 → 3.56.1
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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/.claude/settings.json +9 -9
- package/catalog-manifest.json +2 -2
- package/dist/src/auth/client.js +40 -24
- package/dist/src/commands/agent.js +12 -0
- package/dist/src/commands/analyze.js +18 -14
- package/dist/src/commands/config.js +1 -1
- package/dist/src/commands/daemon.js +11 -19
- package/dist/src/commands/doctor.js +65 -58
- package/dist/src/commands/memory-backup.d.ts +1 -1
- package/dist/src/commands/memory-backup.js +23 -9
- package/dist/src/commands/memory.d.ts +0 -4
- package/dist/src/commands/memory.js +10 -2
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/commands/task.js +12 -0
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-tools/agent-execute-core.js +244 -188
- package/dist/src/mcp-tools/agent-tools.js +15 -16
- package/dist/src/mcp-tools/agentdb-tools.js +22 -3
- package/dist/src/mcp-tools/claims-tools.js +3 -11
- package/dist/src/mcp-tools/config-tools.js +118 -13
- package/dist/src/mcp-tools/hooks-tools.js +47 -2
- package/dist/src/mcp-tools/memory-tools.js +29 -10
- package/dist/src/mcp-tools/record-store.d.ts +3 -0
- package/dist/src/mcp-tools/record-store.js +21 -0
- package/dist/src/mcp-tools/session-tools.js +47 -13
- package/dist/src/mcp-tools/task-tools.js +5 -11
- package/dist/src/mcp-tools/workflow-tools.js +78 -29
- package/dist/src/memory/append-conditions.d.ts +28 -0
- package/dist/src/memory/append-conditions.js +56 -0
- package/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/dist/src/memory/graph-edge-writer.js +90 -0
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +50 -9
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +100 -62
- package/dist/src/memory/memory-root.d.ts +2 -0
- package/dist/src/memory/memory-root.js +28 -0
- package/dist/src/memory/sona-optimizer.d.ts +2 -0
- package/dist/src/memory/sona-optimizer.js +4 -0
- package/dist/src/parser.js +7 -0
- package/dist/src/plugins/store/discovery.d.ts +6 -0
- package/dist/src/plugins/store/discovery.js +7 -3
- package/dist/src/ruvector/neural-router.js +7 -2
- package/dist/src/services/claim-service.js +39 -18
- package/dist/src/services/claims-store.d.ts +3 -0
- package/dist/src/services/claims-store.js +40 -0
- package/dist/src/services/config-file-manager.d.ts +2 -2
- package/dist/src/services/config-file-manager.js +22 -3
- package/dist/src/services/distill-tuning.js +17 -10
- package/dist/src/services/memory-backup.d.ts +7 -0
- package/dist/src/services/memory-backup.js +29 -2
- package/dist/src/services/memory-distillation.js +2 -2
- package/dist/src/services/policy-ledger-anchor.d.ts +8 -0
- package/dist/src/services/policy-ledger-anchor.js +18 -3
- package/dist/src/services/policy-runtime.js +2 -2
- package/dist/src/services/worker-daemon.d.ts +4 -2
- package/dist/src/services/worker-daemon.js +14 -6
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
- package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +4 -4
- package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
- package/.claude/agents/tmp.json +0 -0
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V3 Memory Initializer
|
|
3
|
-
* Properly initializes the memory database with sql.js (WASM SQLite)
|
|
4
|
-
* Includes pattern tables, vector embeddings, migration state tracking
|
|
5
|
-
*
|
|
6
|
-
* ADR-053: Routes through ControllerRegistry → AgentDB v3 when available,
|
|
7
|
-
* falls back to raw sql.js for backwards compatibility.
|
|
8
|
-
*
|
|
9
|
-
* @module v3/cli/memory-initializer
|
|
10
|
-
*/
|
|
11
1
|
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
2
|
+
import { resolveMemoryRoot } from './memory-root.js';
|
|
12
3
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
13
4
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
14
5
|
import * as fs from 'fs';
|
|
@@ -59,25 +50,88 @@ function isRuvectorCoreResolvable() {
|
|
|
59
50
|
return _ruvectorCoreResolvable;
|
|
60
51
|
}
|
|
61
52
|
/**
|
|
62
|
-
* #2735 — before a whole-image sql.js read-modify-persist (export() +
|
|
63
|
-
* rename over the live database path), refuse if
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
53
|
+
* #2735 / #3161 — before a whole-image sql.js read-modify-persist (export() +
|
|
54
|
+
* rename over the live database path), refuse if a native (better-sqlite3)
|
|
55
|
+
* WAL connection is actually LIVE. `-wal`/`-shm` sidecar presence alone is
|
|
56
|
+
* not proof of that: #3161 found `ruflo doctor`'s readonly diagnostic
|
|
57
|
+
* connections leave orphaned sidecars behind after a plain `.close()` (WAL
|
|
58
|
+
* mode does not checkpoint on close unless the closing connection was the
|
|
59
|
+
* last holder in a state that triggers one), permanently blocking every
|
|
60
|
+
* later write in that project.
|
|
61
|
+
*
|
|
62
|
+
* Liveness is probed in two steps, because a single `wal_checkpoint`
|
|
63
|
+
* busy-check is not sufficient on its own:
|
|
64
|
+
*
|
|
65
|
+
* 1. Open our own probe connection and run `PRAGMA wal_checkpoint(TRUNCATE)`.
|
|
66
|
+
* `busy !== 0` means another connection genuinely holds a lock blocking
|
|
67
|
+
* the checkpoint RIGHT NOW (an open transaction) — unsafe, refuse
|
|
68
|
+
* immediately. This also truncates a genuinely-orphaned WAL as a side
|
|
69
|
+
* effect.
|
|
70
|
+
* 2. If `busy === 0`, close our probe connection and re-check whether the
|
|
71
|
+
* sidecars are still on disk. SQLite only deletes `-wal`/`-shm` when the
|
|
72
|
+
* LAST connection to the database closes. If nothing else is attached,
|
|
73
|
+
* OUR close is that last close and the sidecars vanish — safe, proceed.
|
|
74
|
+
* If they are still present after our own close, some OTHER connection
|
|
75
|
+
* is still attached (even idle, holding no lock — e.g. a foreign native
|
|
76
|
+
* handle that already finished its last statement), so step 1's
|
|
77
|
+
* busy-check alone would have missed it. Refuse.
|
|
70
78
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
* Step 2 is what makes this safe against an idle-but-attached native
|
|
80
|
+
* connection (an open handle holding no lock does not block checkpointing,
|
|
81
|
+
* so step 1 alone would report "safe" even though a real holder is still
|
|
82
|
+
* attached) without resorting to platform-specific process scanning.
|
|
83
|
+
*
|
|
84
|
+
* The common case (no sidecars at all) stays a couple of cheap `existsSync`
|
|
85
|
+
* calls — a DB connection is only opened when a sidecar is actually present
|
|
86
|
+
* to investigate.
|
|
87
|
+
*
|
|
88
|
+
* This is still a scoped-down version of the fuller "scan live process
|
|
89
|
+
* holders" design discussed in #2735: it does not close the narrow
|
|
90
|
+
* assess-then-write race (a native opener could still attach in the gap
|
|
91
|
+
* between this check and the write), but it directly closes the
|
|
92
|
+
* demonstrated corruption mechanism — a whole-image write proceeding while
|
|
93
|
+
* an ALREADY-OPEN native connection genuinely holds the file — with no
|
|
94
|
+
* platform-specific process scanning. Fails closed (treats any probe error
|
|
95
|
+
* as "unsafe") because this is a safety gate, not a best-effort probe.
|
|
79
96
|
*/
|
|
80
|
-
function hasNativeWalSidecars(dbPath) {
|
|
97
|
+
async function hasNativeWalSidecars(dbPath) {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.existsSync(`${dbPath}-wal`) && !fs.existsSync(`${dbPath}-shm`)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
let db;
|
|
107
|
+
try {
|
|
108
|
+
const Database = await loadBetterSqlite3();
|
|
109
|
+
const opened = new Database(dbPath);
|
|
110
|
+
db = opened;
|
|
111
|
+
// Report busy immediately instead of retrying for the driver's default
|
|
112
|
+
// (multi-second) busy_timeout — a live holder's checkpoint-blocking
|
|
113
|
+
// lock is a steady-state fact at probe time, not a transient we should
|
|
114
|
+
// wait out, and this is a safety gate on a hot write path.
|
|
115
|
+
opened.pragma('busy_timeout = 0');
|
|
116
|
+
const [{ busy }] = opened.pragma('wal_checkpoint(TRUNCATE)');
|
|
117
|
+
if (busy !== 0)
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
try {
|
|
125
|
+
db?.close();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Nothing was mid-transaction at probe time, and our own (now-closed)
|
|
132
|
+
// probe connection just truncated the WAL. If the sidecars are still
|
|
133
|
+
// here, another connection is still attached and kept them alive through
|
|
134
|
+
// our close.
|
|
81
135
|
try {
|
|
82
136
|
return fs.existsSync(`${dbPath}-wal`) || fs.existsSync(`${dbPath}-shm`);
|
|
83
137
|
}
|
|
@@ -123,34 +177,7 @@ let _memoryRootCache;
|
|
|
123
177
|
export function getMemoryRoot() {
|
|
124
178
|
if (_memoryRootCache !== undefined)
|
|
125
179
|
return _memoryRootCache;
|
|
126
|
-
|
|
127
|
-
const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
128
|
-
if (envPath && envPath.trim().length > 0) {
|
|
129
|
-
_memoryRootCache = path.resolve(envPath);
|
|
130
|
-
return _memoryRootCache;
|
|
131
|
-
}
|
|
132
|
-
// 2. Config file (claude-flow.config.json)
|
|
133
|
-
const configCandidates = [
|
|
134
|
-
path.resolve(process.cwd(), 'claude-flow.config.json'),
|
|
135
|
-
path.resolve(process.cwd(), '.claude-flow', 'config.json'),
|
|
136
|
-
];
|
|
137
|
-
for (const configPath of configCandidates) {
|
|
138
|
-
if (!fs.existsSync(configPath))
|
|
139
|
-
continue;
|
|
140
|
-
try {
|
|
141
|
-
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
142
|
-
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
143
|
-
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
|
|
144
|
-
_memoryRootCache = path.resolve(fromConfig);
|
|
145
|
-
return _memoryRootCache;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
/* malformed config — fall through to default */
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// 3. Default
|
|
153
|
-
_memoryRootCache = path.resolve(process.cwd(), '.swarm');
|
|
180
|
+
_memoryRootCache = resolveMemoryRoot(process.cwd());
|
|
154
181
|
return _memoryRootCache;
|
|
155
182
|
}
|
|
156
183
|
/** For tests + the `memory configure` flow that mutates the config at runtime. */
|
|
@@ -1208,7 +1235,11 @@ export async function ensureSchemaColumns(dbPath, options = {}) {
|
|
|
1208
1235
|
if (columnsAdded.includes('status') || existingColumns.has('status')) {
|
|
1209
1236
|
try {
|
|
1210
1237
|
db.run(`UPDATE memory_entries SET status = 'active' WHERE status IS NULL`);
|
|
1211
|
-
|
|
1238
|
+
// #3918: only a backfill that changed rows dirties the image. Setting
|
|
1239
|
+
// this unconditionally rewrote every up-to-date store on each open (and,
|
|
1240
|
+
// with encryption at rest, with a fresh nonce, so the bytes changed too).
|
|
1241
|
+
if (db.getRowsModified() > 0)
|
|
1242
|
+
modified = true;
|
|
1212
1243
|
}
|
|
1213
1244
|
catch {
|
|
1214
1245
|
/* table is read-only or doesn't exist — skip */
|
|
@@ -2532,6 +2563,9 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
2532
2563
|
* This bypasses MCP and writes directly to the database
|
|
2533
2564
|
*/
|
|
2534
2565
|
export async function storeEntry(options) {
|
|
2566
|
+
if (options.appendOnly && !options.requireNative) {
|
|
2567
|
+
return { success: false, id: '', error: 'Immutable append requires a native writer' };
|
|
2568
|
+
}
|
|
2535
2569
|
// ADR-323: validate before touching either backend so an invalid value
|
|
2536
2570
|
// gets one clear error instead of a raw SQLite CHECK-constraint failure
|
|
2537
2571
|
// from whichever path (bridge vs sql.js) happens to run.
|
|
@@ -2563,8 +2597,12 @@ export async function storeEntry(options) {
|
|
|
2563
2597
|
return bridgeResult;
|
|
2564
2598
|
}
|
|
2565
2599
|
}
|
|
2600
|
+
if (options.requireNative || options.appendConditions) {
|
|
2601
|
+
return { success: false, id: '', error: 'Native memory writer required; refusing whole-image sql.js fallback' };
|
|
2602
|
+
}
|
|
2566
2603
|
// Fallback: raw sql.js
|
|
2567
|
-
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false, provenanceType } = options;
|
|
2604
|
+
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert: requestedUpsert = false, provenanceType } = options;
|
|
2605
|
+
const upsert = requestedUpsert && !options.appendOnly;
|
|
2568
2606
|
const swarmDir = getMemoryRoot();
|
|
2569
2607
|
const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
|
|
2570
2608
|
try {
|
|
@@ -2578,7 +2616,7 @@ export async function storeEntry(options) {
|
|
|
2578
2616
|
// race). This check gates ensureSchemaColumns()'s own whole-image
|
|
2579
2617
|
// write below too, not just this function's.
|
|
2580
2618
|
await releaseOwnNativeHandle(dbPath);
|
|
2581
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
2619
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
2582
2620
|
return {
|
|
2583
2621
|
success: false,
|
|
2584
2622
|
id: '',
|
|
@@ -2985,7 +3023,7 @@ export async function listEntries(options) {
|
|
|
2985
3023
|
// Listing can migrate/backfill the schema, so it is also a whole-image
|
|
2986
3024
|
// writer. The newly selected native mirror may still have a live WAL.
|
|
2987
3025
|
await releaseOwnNativeHandle(dbPath);
|
|
2988
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3026
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
2989
3027
|
return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
|
|
2990
3028
|
}
|
|
2991
3029
|
// Ensure schema has all required columns (migration for older DBs)
|
|
@@ -3097,7 +3135,7 @@ export async function getEntry(options) {
|
|
|
3097
3135
|
// bump is itself a whole-image write, not a lightweight read, even
|
|
3098
3136
|
// though this function's contract reads as a "get".
|
|
3099
3137
|
await releaseOwnNativeHandle(dbPath);
|
|
3100
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3138
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3101
3139
|
return {
|
|
3102
3140
|
success: false,
|
|
3103
3141
|
found: false,
|
|
@@ -3217,7 +3255,7 @@ export async function deleteEntry(options) {
|
|
|
3217
3255
|
// #2735 — see storeEntry's identical gate for the corruption mechanism
|
|
3218
3256
|
// this closes.
|
|
3219
3257
|
await releaseOwnNativeHandle(dbPath);
|
|
3220
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3258
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3221
3259
|
return {
|
|
3222
3260
|
success: false,
|
|
3223
3261
|
deleted: false,
|
|
@@ -3437,7 +3475,7 @@ export async function purgeNamespace(options) {
|
|
|
3437
3475
|
}
|
|
3438
3476
|
// Recheck at mutation time even when the CLI already read a preview.
|
|
3439
3477
|
await releaseOwnNativeHandle(dbPath);
|
|
3440
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3478
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3441
3479
|
return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
|
|
3442
3480
|
}
|
|
3443
3481
|
await ensureSchemaColumns(dbPath, options);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Uncached memory-directory authority for an explicit project directory. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export function resolveMemoryRoot(cwd) {
|
|
5
|
+
const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
6
|
+
if (envPath && envPath.trim().length > 0)
|
|
7
|
+
return path.resolve(cwd, envPath);
|
|
8
|
+
const configCandidates = [
|
|
9
|
+
path.resolve(cwd, 'claude-flow.config.json'),
|
|
10
|
+
path.resolve(cwd, '.claude-flow', 'config.json'),
|
|
11
|
+
];
|
|
12
|
+
for (const configPath of configCandidates) {
|
|
13
|
+
if (!fs.existsSync(configPath))
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
17
|
+
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
18
|
+
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
|
|
19
|
+
return path.resolve(cwd, fromConfig);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* malformed config — fall through to default */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(cwd, '.swarm');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=memory-root.js.map
|
|
@@ -93,6 +93,8 @@ export interface SONAStats {
|
|
|
93
93
|
agents: number;
|
|
94
94
|
} | 'unavailable';
|
|
95
95
|
}
|
|
96
|
+
/** Agent roles supported by the SONA routing optimizer. */
|
|
97
|
+
export declare function isSupportedSonaAgent(agent: string): boolean;
|
|
96
98
|
/**
|
|
97
99
|
* SONA Optimizer for adaptive routing based on trajectory outcomes
|
|
98
100
|
*
|
|
@@ -61,6 +61,10 @@ const AGENT_TYPES = [
|
|
|
61
61
|
'security-architect',
|
|
62
62
|
'performance-engineer',
|
|
63
63
|
];
|
|
64
|
+
/** Agent roles supported by the SONA routing optimizer. */
|
|
65
|
+
export function isSupportedSonaAgent(agent) {
|
|
66
|
+
return AGENT_TYPES.includes(agent);
|
|
67
|
+
}
|
|
64
68
|
/**
|
|
65
69
|
* Task keywords for pattern extraction
|
|
66
70
|
*/
|
package/dist/src/parser.js
CHANGED
|
@@ -568,6 +568,13 @@ export class CommandParser {
|
|
|
568
568
|
if (opt.required && (flags[key] === undefined || flags[key] === '')) {
|
|
569
569
|
errors.push(`Required option missing: --${opt.name}`);
|
|
570
570
|
}
|
|
571
|
+
// A value-taking option given with nothing after it (`--task`, `--key` at the end, or
|
|
572
|
+
// followed by another flag) is parsed as boolean `true`. Commands then treat `true` as
|
|
573
|
+
// the value: `hooks route --task` crashed with "task.trim is not a function" and
|
|
574
|
+
// `memory retrieve --key` looked up the key "true". Report it instead.
|
|
575
|
+
if ((opt.type === 'string' || opt.type === 'number') && flags[key] === true) {
|
|
576
|
+
errors.push(`Option --${opt.name} needs a value`);
|
|
577
|
+
}
|
|
571
578
|
// Check choices
|
|
572
579
|
if (opt.choices && flags[key] !== undefined) {
|
|
573
580
|
const value = String(flags[key]);
|
|
@@ -32,6 +32,12 @@ export interface PluginDiscoveryResult {
|
|
|
32
32
|
source?: string;
|
|
33
33
|
fromCache?: boolean;
|
|
34
34
|
error?: string;
|
|
35
|
+
/**
|
|
36
|
+
* True when the signed registry could not be fetched or verified and the CLI fell back to
|
|
37
|
+
* its built-in plugin list. That list is safe to use (fixed, first-party npm names), but it
|
|
38
|
+
* is NOT the verified registry, and callers must say so rather than report a discovery.
|
|
39
|
+
*/
|
|
40
|
+
demo?: boolean;
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
37
43
|
* Plugin Discovery Service
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Discovers plugin registries via IPNS and fetches from IPFS
|
|
4
4
|
* Parallel implementation to pattern store for plugins
|
|
5
5
|
*/
|
|
6
|
-
import * as crypto from 'crypto';
|
|
7
6
|
import { resolveIPNS, fetchFromIPFS, verifyEd25519Signature } from '../../transfer/ipfs/client.js';
|
|
8
7
|
/**
|
|
9
8
|
* Fetch real npm download stats for a package
|
|
@@ -115,7 +114,9 @@ export class PluginDiscoveryService {
|
|
|
115
114
|
success: true,
|
|
116
115
|
registry: cached.registry,
|
|
117
116
|
fromCache: true,
|
|
118
|
-
|
|
117
|
+
// A cached fallback is still the fallback: never relabel it as the real registry.
|
|
118
|
+
source: cached.demo ? `${registry.name} (demo)` : registry.name,
|
|
119
|
+
...(cached.demo ? { demo: true } : {}),
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
try {
|
|
@@ -223,13 +224,16 @@ export class PluginDiscoveryService {
|
|
|
223
224
|
this.cache.set(registry.ipnsName, {
|
|
224
225
|
registry: demoRegistry,
|
|
225
226
|
timestamp: Date.now(),
|
|
227
|
+
demo: true,
|
|
226
228
|
});
|
|
227
229
|
return {
|
|
228
230
|
success: true,
|
|
229
231
|
registry: demoRegistry,
|
|
230
|
-
|
|
232
|
+
// No CID: the built-in list was not fetched from IPFS. (This used to be a random
|
|
233
|
+
// `bafybeiplugin…` string, printed as "Registry CID" as if it were a real address.)
|
|
231
234
|
source: `${registry.name} (demo)`,
|
|
232
235
|
fromCache: false,
|
|
236
|
+
demo: true,
|
|
233
237
|
};
|
|
234
238
|
}
|
|
235
239
|
/**
|
|
@@ -288,8 +288,13 @@ async function resolveBackend(cfg) {
|
|
|
288
288
|
return r;
|
|
289
289
|
return {
|
|
290
290
|
route: (e) => {
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
// Calibration can move a candidate across the quality bar. Select
|
|
292
|
+
// from the calibrated predictions, not the raw router's old pick.
|
|
293
|
+
const all = r.predictAll(e).map(c => ({ ...c, predictedQuality: cal.transform(c.predictedQuality) }));
|
|
294
|
+
const clearing = all.filter(c => c.predictedQuality >= cfg.qualityBar)
|
|
295
|
+
.sort((a, b) => a.costPerMTok - b.costPerMTok);
|
|
296
|
+
const pick = clearing[0] ?? [...all].sort((a, b) => b.predictedQuality - a.predictedQuality)[0];
|
|
297
|
+
return { ...pick, metBar: pick.predictedQuality >= cfg.qualityBar };
|
|
293
298
|
},
|
|
294
299
|
predictAll: (e) => r.predictAll(e).map(c => ({ ...c, predictedQuality: cal.transform(c.predictedQuality) })),
|
|
295
300
|
};
|
|
@@ -16,6 +16,7 @@ import { EventEmitter } from 'events';
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { execFileSync } from 'child_process';
|
|
19
|
+
import { readClaimsStore } from './claims-store.js';
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// Default Configuration
|
|
21
22
|
// ============================================================================
|
|
@@ -57,28 +58,29 @@ export class ClaimService extends EventEmitter {
|
|
|
57
58
|
// Load existing claims
|
|
58
59
|
await this.loadClaims();
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Start fresh if file is corrupted
|
|
75
|
-
}
|
|
61
|
+
loadClaims() {
|
|
62
|
+
const data = readClaimsStore(path.join(this.storagePath, 'claims.json'));
|
|
63
|
+
const claims = new Map();
|
|
64
|
+
const stealable = new Map();
|
|
65
|
+
for (const claim of Object.values(data.claims)) {
|
|
66
|
+
claim.claimedAt = new Date(claim.claimedAt);
|
|
67
|
+
claim.statusChangedAt = new Date(claim.statusChangedAt);
|
|
68
|
+
if (claim.expiresAt)
|
|
69
|
+
claim.expiresAt = new Date(claim.expiresAt);
|
|
70
|
+
claims.set(claim.issueId, claim);
|
|
71
|
+
}
|
|
72
|
+
for (const [id, info] of Object.entries(data.stealable)) {
|
|
73
|
+
stealable.set(id, { ...info, stealableAt: new Date(info.stealableAt) });
|
|
76
74
|
}
|
|
75
|
+
this.claims = claims;
|
|
76
|
+
this.stealableInfo = stealable;
|
|
77
77
|
}
|
|
78
78
|
async saveClaims() {
|
|
79
79
|
const claimsFile = path.join(this.storagePath, 'claims.json');
|
|
80
80
|
const data = {
|
|
81
|
-
|
|
81
|
+
...readClaimsStore(claimsFile),
|
|
82
|
+
claims: Object.fromEntries(this.claims),
|
|
83
|
+
stealable: Object.fromEntries(this.stealableInfo),
|
|
82
84
|
savedAt: new Date().toISOString(),
|
|
83
85
|
};
|
|
84
86
|
fs.writeFileSync(claimsFile, JSON.stringify(data, null, 2));
|
|
@@ -87,6 +89,7 @@ export class ClaimService extends EventEmitter {
|
|
|
87
89
|
// Core Claiming
|
|
88
90
|
// ==========================================================================
|
|
89
91
|
async claim(issueId, claimant) {
|
|
92
|
+
this.loadClaims();
|
|
90
93
|
// Check if already claimed
|
|
91
94
|
const existing = this.claims.get(issueId);
|
|
92
95
|
if (existing && existing.status !== 'stealable') {
|
|
@@ -116,6 +119,7 @@ export class ClaimService extends EventEmitter {
|
|
|
116
119
|
return { success: true, claim };
|
|
117
120
|
}
|
|
118
121
|
async release(issueId, claimant) {
|
|
122
|
+
this.loadClaims();
|
|
119
123
|
const claim = this.claims.get(issueId);
|
|
120
124
|
if (!claim) {
|
|
121
125
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -137,6 +141,7 @@ export class ClaimService extends EventEmitter {
|
|
|
137
141
|
// Handoffs
|
|
138
142
|
// ==========================================================================
|
|
139
143
|
async requestHandoff(issueId, from, to, reason) {
|
|
144
|
+
this.loadClaims();
|
|
140
145
|
const claim = this.claims.get(issueId);
|
|
141
146
|
if (!claim) {
|
|
142
147
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -158,6 +163,7 @@ export class ClaimService extends EventEmitter {
|
|
|
158
163
|
});
|
|
159
164
|
}
|
|
160
165
|
async acceptHandoff(issueId, claimant) {
|
|
166
|
+
this.loadClaims();
|
|
161
167
|
const claim = this.claims.get(issueId);
|
|
162
168
|
if (!claim || claim.status !== 'handoff-pending') {
|
|
163
169
|
throw new Error(`No pending handoff for issue ${issueId}`);
|
|
@@ -181,6 +187,7 @@ export class ClaimService extends EventEmitter {
|
|
|
181
187
|
});
|
|
182
188
|
}
|
|
183
189
|
async rejectHandoff(issueId, claimant, reason) {
|
|
190
|
+
this.loadClaims();
|
|
184
191
|
const claim = this.claims.get(issueId);
|
|
185
192
|
if (!claim || claim.status !== 'handoff-pending') {
|
|
186
193
|
throw new Error(`No pending handoff for issue ${issueId}`);
|
|
@@ -205,6 +212,7 @@ export class ClaimService extends EventEmitter {
|
|
|
205
212
|
// Status Updates
|
|
206
213
|
// ==========================================================================
|
|
207
214
|
async updateStatus(issueId, status, note) {
|
|
215
|
+
this.loadClaims();
|
|
208
216
|
const claim = this.claims.get(issueId);
|
|
209
217
|
if (!claim) {
|
|
210
218
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -227,6 +235,7 @@ export class ClaimService extends EventEmitter {
|
|
|
227
235
|
});
|
|
228
236
|
}
|
|
229
237
|
async updateProgress(issueId, progress) {
|
|
238
|
+
this.loadClaims();
|
|
230
239
|
const claim = this.claims.get(issueId);
|
|
231
240
|
if (!claim) {
|
|
232
241
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -235,6 +244,7 @@ export class ClaimService extends EventEmitter {
|
|
|
235
244
|
await this.saveClaims();
|
|
236
245
|
}
|
|
237
246
|
async requestReview(issueId, reviewers) {
|
|
247
|
+
this.loadClaims();
|
|
238
248
|
const claim = this.claims.get(issueId);
|
|
239
249
|
if (!claim) {
|
|
240
250
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -254,6 +264,7 @@ export class ClaimService extends EventEmitter {
|
|
|
254
264
|
// Work Stealing
|
|
255
265
|
// ==========================================================================
|
|
256
266
|
async markStealable(issueId, info) {
|
|
267
|
+
this.loadClaims();
|
|
257
268
|
const claim = this.claims.get(issueId);
|
|
258
269
|
if (!claim) {
|
|
259
270
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -273,6 +284,7 @@ export class ClaimService extends EventEmitter {
|
|
|
273
284
|
});
|
|
274
285
|
}
|
|
275
286
|
async steal(issueId, stealer) {
|
|
287
|
+
this.loadClaims();
|
|
276
288
|
const claim = this.claims.get(issueId);
|
|
277
289
|
if (!claim) {
|
|
278
290
|
return { success: false, error: `Issue ${issueId} is not claimed` };
|
|
@@ -309,6 +321,7 @@ export class ClaimService extends EventEmitter {
|
|
|
309
321
|
return { success: true, claim, previousOwner, context: info };
|
|
310
322
|
}
|
|
311
323
|
async getStealable(agentType) {
|
|
324
|
+
this.loadClaims();
|
|
312
325
|
const stealable = [];
|
|
313
326
|
for (const claim of this.claims.values()) {
|
|
314
327
|
if (claim.status !== 'stealable')
|
|
@@ -323,6 +336,7 @@ export class ClaimService extends EventEmitter {
|
|
|
323
336
|
return stealable;
|
|
324
337
|
}
|
|
325
338
|
async contestSteal(issueId, originalClaimant, reason) {
|
|
339
|
+
this.loadClaims();
|
|
326
340
|
const claim = this.claims.get(issueId);
|
|
327
341
|
if (!claim) {
|
|
328
342
|
throw new Error(`Issue ${issueId} is not claimed`);
|
|
@@ -340,6 +354,7 @@ export class ClaimService extends EventEmitter {
|
|
|
340
354
|
// Load Balancing
|
|
341
355
|
// ==========================================================================
|
|
342
356
|
async getAgentLoad(agentId) {
|
|
357
|
+
this.loadClaims();
|
|
343
358
|
const claims = [];
|
|
344
359
|
let blockedCount = 0;
|
|
345
360
|
for (const claim of this.claims.values()) {
|
|
@@ -362,6 +377,7 @@ export class ClaimService extends EventEmitter {
|
|
|
362
377
|
};
|
|
363
378
|
}
|
|
364
379
|
async rebalance(swarmId) {
|
|
380
|
+
this.loadClaims();
|
|
365
381
|
const result = { moved: [], suggested: [] };
|
|
366
382
|
// Get all agent loads
|
|
367
383
|
const agentLoads = new Map();
|
|
@@ -410,6 +426,7 @@ export class ClaimService extends EventEmitter {
|
|
|
410
426
|
// Queries
|
|
411
427
|
// ==========================================================================
|
|
412
428
|
async getClaimedBy(claimant) {
|
|
429
|
+
this.loadClaims();
|
|
413
430
|
return Array.from(this.claims.values()).filter(c => this.isSameClaimant(c.claimant, claimant));
|
|
414
431
|
}
|
|
415
432
|
async getAvailableIssues(_filters) {
|
|
@@ -418,18 +435,22 @@ export class ClaimService extends EventEmitter {
|
|
|
418
435
|
return [];
|
|
419
436
|
}
|
|
420
437
|
async getIssueStatus(issueId) {
|
|
438
|
+
this.loadClaims();
|
|
421
439
|
return this.claims.get(issueId) || null;
|
|
422
440
|
}
|
|
423
441
|
async getAllClaims() {
|
|
442
|
+
this.loadClaims();
|
|
424
443
|
return Array.from(this.claims.values());
|
|
425
444
|
}
|
|
426
445
|
async getByStatus(status) {
|
|
446
|
+
this.loadClaims();
|
|
427
447
|
return Array.from(this.claims.values()).filter(c => c.status === status);
|
|
428
448
|
}
|
|
429
449
|
// ==========================================================================
|
|
430
450
|
// Auto-Management
|
|
431
451
|
// ==========================================================================
|
|
432
452
|
async expireStale(maxAgeMinutes) {
|
|
453
|
+
this.loadClaims();
|
|
433
454
|
const threshold = maxAgeMinutes ?? this.config.staleThresholdMinutes;
|
|
434
455
|
const now = Date.now();
|
|
435
456
|
const expired = [];
|
|
@@ -445,7 +466,7 @@ export class ClaimService extends EventEmitter {
|
|
|
445
466
|
progress: claim.progress,
|
|
446
467
|
context: `Stale: No activity for ${Math.round(age)} minutes`,
|
|
447
468
|
});
|
|
448
|
-
expired.push(claim);
|
|
469
|
+
expired.push(this.claims.get(claim.issueId));
|
|
449
470
|
}
|
|
450
471
|
}
|
|
451
472
|
return expired;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Shared on-disk shape for the CLI claim service and public MCP claim tools. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** Read legacy CLI arrays and MCP dictionaries; never replace unreadable state. */
|
|
7
|
+
export function readClaimsStore(file) {
|
|
8
|
+
let text;
|
|
9
|
+
try {
|
|
10
|
+
text = readFileSync(file, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return { claims: {}, stealable: {}, contests: {} };
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
const data = JSON.parse(text);
|
|
18
|
+
if (!record(data) || (!Array.isArray(data.claims) && !record(data.claims))) {
|
|
19
|
+
throw new Error('Invalid claims store: expected a claims array or object');
|
|
20
|
+
}
|
|
21
|
+
const entries = Array.isArray(data.claims)
|
|
22
|
+
? data.claims.map((claim) => [record(claim) ? claim.issueId : undefined, claim])
|
|
23
|
+
: Object.entries(data.claims);
|
|
24
|
+
const claims = Object.create(null);
|
|
25
|
+
for (const [id, claim] of entries) {
|
|
26
|
+
if (typeof id !== 'string' || !id || !record(claim) || claim.issueId !== id ||
|
|
27
|
+
!record(claim.claimant) || typeof claim.status !== 'string' ||
|
|
28
|
+
typeof claim.claimedAt !== 'string' || typeof claim.statusChangedAt !== 'string' ||
|
|
29
|
+
Object.hasOwn(claims, id)) {
|
|
30
|
+
throw new Error('Invalid claims store: malformed or duplicate claim');
|
|
31
|
+
}
|
|
32
|
+
claims[id] = claim;
|
|
33
|
+
}
|
|
34
|
+
for (const key of ['stealable', 'contests']) {
|
|
35
|
+
if (data[key] !== undefined && !record(data[key]))
|
|
36
|
+
throw new Error(`Invalid claims store: ${key} must be an object`);
|
|
37
|
+
}
|
|
38
|
+
return { ...data, claims, stealable: data.stealable ?? {}, contests: data.contests ?? {} };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=claims-store.js.map
|
|
@@ -18,8 +18,8 @@ export declare class ConfigFileManager {
|
|
|
18
18
|
set(cwd: string, key: string, value: unknown): void;
|
|
19
19
|
/** Create a new config file with defaults */
|
|
20
20
|
create(cwd: string, overrides?: Record<string, unknown>, force?: boolean): string;
|
|
21
|
-
/** Reset
|
|
22
|
-
reset(cwd: string): string;
|
|
21
|
+
/** Reset all configuration or only the selected section to defaults. */
|
|
22
|
+
reset(cwd: string, section?: string): string;
|
|
23
23
|
/** Export config to a specific path */
|
|
24
24
|
exportTo(cwd: string, exportPath: string): void;
|
|
25
25
|
/** Import config from a specific path */
|