@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.
Files changed (71) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.js +40 -24
  5. package/dist/src/commands/agent.js +12 -0
  6. package/dist/src/commands/analyze.js +18 -14
  7. package/dist/src/commands/config.js +1 -1
  8. package/dist/src/commands/daemon.js +11 -19
  9. package/dist/src/commands/doctor.js +65 -58
  10. package/dist/src/commands/memory-backup.d.ts +1 -1
  11. package/dist/src/commands/memory-backup.js +23 -9
  12. package/dist/src/commands/memory.d.ts +0 -4
  13. package/dist/src/commands/memory.js +10 -2
  14. package/dist/src/commands/plugins.js +13 -1
  15. package/dist/src/commands/task.js +12 -0
  16. package/dist/src/init/settings-generator.js +3 -3
  17. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  18. package/dist/src/mcp-tools/agent-tools.js +15 -16
  19. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  20. package/dist/src/mcp-tools/claims-tools.js +3 -11
  21. package/dist/src/mcp-tools/config-tools.js +118 -13
  22. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  23. package/dist/src/mcp-tools/memory-tools.js +29 -10
  24. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  25. package/dist/src/mcp-tools/record-store.js +21 -0
  26. package/dist/src/mcp-tools/session-tools.js +47 -13
  27. package/dist/src/mcp-tools/task-tools.js +5 -11
  28. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  29. package/dist/src/memory/append-conditions.d.ts +28 -0
  30. package/dist/src/memory/append-conditions.js +56 -0
  31. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  32. package/dist/src/memory/graph-edge-writer.js +90 -0
  33. package/dist/src/memory/memory-bridge.d.ts +4 -0
  34. package/dist/src/memory/memory-bridge.js +50 -9
  35. package/dist/src/memory/memory-initializer.d.ts +4 -0
  36. package/dist/src/memory/memory-initializer.js +100 -62
  37. package/dist/src/memory/memory-root.d.ts +2 -0
  38. package/dist/src/memory/memory-root.js +28 -0
  39. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  40. package/dist/src/memory/sona-optimizer.js +4 -0
  41. package/dist/src/parser.js +7 -0
  42. package/dist/src/plugins/store/discovery.d.ts +6 -0
  43. package/dist/src/plugins/store/discovery.js +7 -3
  44. package/dist/src/ruvector/neural-router.js +7 -2
  45. package/dist/src/services/claim-service.js +39 -18
  46. package/dist/src/services/claims-store.d.ts +3 -0
  47. package/dist/src/services/claims-store.js +40 -0
  48. package/dist/src/services/config-file-manager.d.ts +2 -2
  49. package/dist/src/services/config-file-manager.js +22 -3
  50. package/dist/src/services/distill-tuning.js +17 -10
  51. package/dist/src/services/memory-backup.d.ts +7 -0
  52. package/dist/src/services/memory-backup.js +29 -2
  53. package/dist/src/services/memory-distillation.js +2 -2
  54. package/dist/src/services/policy-ledger-anchor.d.ts +8 -0
  55. package/dist/src/services/policy-ledger-anchor.js +18 -3
  56. package/dist/src/services/policy-runtime.js +2 -2
  57. package/dist/src/services/worker-daemon.d.ts +4 -2
  58. package/dist/src/services/worker-daemon.js +14 -6
  59. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  60. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  61. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  62. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  63. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  64. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  65. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  66. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  67. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  68. package/node_modules/@claude-flow/security/package.json +1 -1
  69. package/package.json +4 -4
  70. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  71. 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 there is evidence of a
64
- * live native (better-sqlite3) WAL connection: `-wal`/`-shm` sidecar files
65
- * on disk. A native connection in WAL mode keeps its sidecars present for
66
- * its entire lifetime (removed only on the last connection's clean close),
67
- * so their presence is strong evidence of a live native holder — and their
68
- * absence means the image is a clean, checkpointed, standalone file that
69
- * sql.js can safely read-modify-write.
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
- * This is a scoped-down version of the fuller "scan live process holders"
72
- * design discussed in #2735: it does not close the narrow assess-then-write
73
- * race (a native opener could still attach in the gap between this check
74
- * and the write), but it directly closes the demonstrated corruption
75
- * mechanism — a whole-image write proceeding while an ALREADY-OPEN native
76
- * connection's sidecars are on disk — with no platform-specific process
77
- * scanning. Fails closed (treats a stat error as "unsafe") because this is
78
- * a safety gate, not a best-effort probe.
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
- // 1. Env var
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
- modified = true;
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,2 @@
1
+ export declare function resolveMemoryRoot(cwd: string): string;
2
+ //# sourceMappingURL=memory-root.d.ts.map
@@ -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
  */
@@ -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
- source: registry.name,
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
- cid: `bafybeiplugin${crypto.randomBytes(16).toString('hex')}`,
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
- const result = r.route(e);
292
- return { ...result, predictedQuality: cal.transform(result.predictedQuality) };
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
- async loadClaims() {
61
- const claimsFile = path.join(this.storagePath, 'claims.json');
62
- if (fs.existsSync(claimsFile)) {
63
- try {
64
- const data = JSON.parse(fs.readFileSync(claimsFile, 'utf-8'));
65
- for (const claim of 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
- this.claims.set(claim.issueId, claim);
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
- claims: Array.from(this.claims.values()),
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,3 @@
1
+ /** Read legacy CLI arrays and MCP dictionaries; never replace unreadable state. */
2
+ export declare function readClaimsStore(file: string): Record<string, unknown>;
3
+ //# sourceMappingURL=claims-store.d.ts.map
@@ -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 config to defaults */
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 */