@claude-flow/cli 3.54.1 → 3.56.0

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 (116) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/bin/cli.js +1 -1
  4. package/bin/mcp-server.js +1 -1
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/auth/client.js +40 -24
  7. package/dist/src/commands/agent.js +12 -0
  8. package/dist/src/commands/analyze.js +18 -14
  9. package/dist/src/commands/autopilot.js +2 -2
  10. package/dist/src/commands/daemon.js +15 -3
  11. package/dist/src/commands/doctor.js +65 -58
  12. package/dist/src/commands/hive-mind.js +7 -0
  13. package/dist/src/commands/mcp.js +35 -1
  14. package/dist/src/commands/memory-backup.d.ts +1 -1
  15. package/dist/src/commands/memory-backup.js +23 -9
  16. package/dist/src/commands/memory.d.ts +0 -4
  17. package/dist/src/commands/memory.js +10 -2
  18. package/dist/src/commands/plugins.js +13 -1
  19. package/dist/src/commands/policy.js +10 -2
  20. package/dist/src/commands/task.js +12 -0
  21. package/dist/src/init/settings-generator.js +3 -3
  22. package/dist/src/mcp-server.d.ts +33 -0
  23. package/dist/src/mcp-server.js +96 -4
  24. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  25. package/dist/src/mcp-tools/agent-tools.js +15 -16
  26. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  27. package/dist/src/mcp-tools/claims-tools.js +3 -11
  28. package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
  29. package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
  30. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  31. package/dist/src/mcp-tools/memory-tools.js +29 -10
  32. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  33. package/dist/src/mcp-tools/record-store.js +21 -0
  34. package/dist/src/mcp-tools/session-tools.js +47 -13
  35. package/dist/src/mcp-tools/task-tools.js +5 -11
  36. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  37. package/dist/src/memory/append-conditions.d.ts +28 -0
  38. package/dist/src/memory/append-conditions.js +56 -0
  39. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  40. package/dist/src/memory/graph-edge-writer.js +90 -0
  41. package/dist/src/memory/memory-bridge.d.ts +4 -0
  42. package/dist/src/memory/memory-bridge.js +50 -9
  43. package/dist/src/memory/memory-initializer.d.ts +4 -0
  44. package/dist/src/memory/memory-initializer.js +95 -61
  45. package/dist/src/memory/memory-root.d.ts +2 -0
  46. package/dist/src/memory/memory-root.js +28 -0
  47. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  48. package/dist/src/memory/sona-optimizer.js +4 -0
  49. package/dist/src/missions/schemas.d.ts +125 -125
  50. package/dist/src/mods/command-registry/schema.d.ts +199 -199
  51. package/dist/src/parser.d.ts +3 -0
  52. package/dist/src/parser.js +71 -13
  53. package/dist/src/plugins/store/discovery.d.ts +6 -0
  54. package/dist/src/plugins/store/discovery.js +7 -3
  55. package/dist/src/production/retry.js +1 -1
  56. package/dist/src/ruvector/neural-router.js +7 -2
  57. package/dist/src/services/bounded-worker-pool.js +3 -0
  58. package/dist/src/services/claim-service.js +39 -18
  59. package/dist/src/services/claims-store.d.ts +3 -0
  60. package/dist/src/services/claims-store.js +40 -0
  61. package/dist/src/services/config-file-manager.d.ts +3 -0
  62. package/dist/src/services/config-file-manager.js +24 -7
  63. package/dist/src/services/distill-tuning.js +17 -10
  64. package/dist/src/services/memory-backup.d.ts +7 -0
  65. package/dist/src/services/memory-backup.js +29 -2
  66. package/dist/src/services/memory-distillation.js +2 -2
  67. package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
  68. package/dist/src/services/policy-ledger-anchor.js +163 -0
  69. package/dist/src/services/policy-runtime.d.ts +11 -5
  70. package/dist/src/services/policy-runtime.js +70 -15
  71. package/dist/src/services/worker-daemon.d.ts +5 -2
  72. package/dist/src/services/worker-daemon.js +26 -6
  73. package/dist/src/services/worker-queue.js +11 -2
  74. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  75. package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
  76. package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
  77. package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
  78. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
  79. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  80. package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
  81. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  82. package/node_modules/@claude-flow/mcp/package.json +1 -1
  83. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
  84. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
  85. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  86. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
  87. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
  88. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
  89. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
  90. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
  91. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
  92. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
  93. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  94. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  95. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  96. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  97. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  98. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  99. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  100. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  101. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  102. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
  103. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  104. package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
  105. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  106. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
  107. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  108. package/node_modules/@claude-flow/security/package.json +1 -1
  109. package/package.json +7 -7
  110. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  111. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  112. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
  113. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
  114. package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
  115. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
  116. package/.claude/agents/tmp.json +0 -0
@@ -1154,14 +1154,59 @@ export const hooksRoute = {
1154
1154
  catch {
1155
1155
  // AgentDB router not available — fall through to local routing
1156
1156
  }
1157
+ // Trajectory learning persists SONA outcomes separately from the local
1158
+ // semantic index. Only consume an eligible learned pattern here; native,
1159
+ // Q-learning and keyword suggestions retain their existing routing paths.
1160
+ try {
1161
+ const sonaStart = performance.now();
1162
+ const sona = await getSONAOptimizer();
1163
+ const suggestion = await sona?.getRoutingSuggestion(task);
1164
+ const { isSupportedSonaAgent } = await import('../memory/sona-optimizer.js');
1165
+ if (suggestion?.source === 'sona-pattern'
1166
+ && Number.isFinite(suggestion.confidence)
1167
+ && suggestion.confidence >= 0.6 && suggestion.confidence <= 1
1168
+ && isSupportedSonaAgent(suggestion.agent)) {
1169
+ const confidence = Math.round(suggestion.confidence * 100) / 100;
1170
+ const complexity = task.length > 200 ? 'high' : task.length < 50 ? 'low' : 'medium';
1171
+ return {
1172
+ task,
1173
+ routing: { method: 'sona-pattern', backend: 'SONA persisted keyword patterns', latencyMs: performance.now() - sonaStart, throughput: 'N/A' },
1174
+ matched: true,
1175
+ matchedPattern: 'sona-pattern',
1176
+ semanticMatches: [],
1177
+ primaryAgent: {
1178
+ type: suggestion.agent,
1179
+ confidence,
1180
+ reason: `Persisted SONA outcome pattern (${Math.round(suggestion.confidence * 100)}% confidence)`,
1181
+ },
1182
+ alternativeAgents: suggestion.alternatives
1183
+ .filter(alternative => isSupportedSonaAgent(alternative.agent) && Number.isFinite(alternative.score))
1184
+ .map(alternative => ({
1185
+ type: alternative.agent,
1186
+ confidence: Math.round(alternative.score * 100) / 100,
1187
+ reason: 'Alternative from persisted SONA patterns',
1188
+ })),
1189
+ estimatedMetrics: {
1190
+ successProbability: confidence,
1191
+ estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
1192
+ complexity,
1193
+ },
1194
+ swarmRecommendation: null,
1195
+ learning: { source: suggestion.source, matchedKeywords: suggestion.matchedKeywords },
1196
+ };
1197
+ }
1198
+ }
1199
+ catch {
1200
+ // Optional SONA state unavailable — preserve the existing local route.
1201
+ }
1157
1202
  }
1158
1203
  return routeTaskLocal(task, context, useSemanticRouter);
1159
1204
  }),
1160
1205
  };
1161
1206
  /**
1162
1207
  * hooks_route's local routing (semantic index + keyword fallback), run after the
1163
- * AgentDB pre-route. Shared with {@link routeTaskForBench} so the benchmark
1164
- * measures exactly the path hooks_route takes. (Body kept at its original
1208
+ * AgentDB and eligible SONA pre-routes. Shared with {@link routeTaskForBench}
1209
+ * so the benchmark measures this local path. (Body kept at its original
1165
1210
  * indentation to keep this refactor's diff small.)
1166
1211
  */
1167
1212
  async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride) {
@@ -1500,7 +1500,21 @@ export const memoryTools = [
1500
1500
  // entry body. Without this, `value` is always null because listEntries
1501
1501
  // strips content by default (callers pay for the JSON parse only when
1502
1502
  // they need it).
1503
- const all = await listEntries({ limit: 100000, namespace, includeContent: true });
1503
+ // A successful export must cover the whole reported store. Read all
1504
+ // pages before touching an existing snapshot, including on read failure.
1505
+ const entries = [];
1506
+ let total;
1507
+ do {
1508
+ const page = await listEntries({ limit: 10000, offset: entries.length, namespace, includeContent: true });
1509
+ if (!page.success)
1510
+ return { error: page.error || 'Could not read memory for export' };
1511
+ total ??= page.total;
1512
+ if (page.entries.length === 0 && entries.length < total) {
1513
+ return { error: `Incomplete memory export: read ${entries.length} of ${total} entries` };
1514
+ }
1515
+ entries.push(...page.entries);
1516
+ } while (entries.length < total);
1517
+ const all = { entries };
1504
1518
  const payload = {
1505
1519
  schema: 'ruflo-memory-export/v1',
1506
1520
  exportedAt: new Date().toISOString(),
@@ -1595,6 +1609,8 @@ export const memoryTools = [
1595
1609
  }
1596
1610
  let imported = 0;
1597
1611
  let skipped = 0;
1612
+ let failed = 0;
1613
+ const errors = [];
1598
1614
  for (const e of entries) {
1599
1615
  if (!e || typeof e.key !== 'string') {
1600
1616
  skipped++;
@@ -1603,19 +1619,22 @@ export const memoryTools = [
1603
1619
  const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
1604
1620
  try {
1605
1621
  const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
1606
- if (result.success) {
1607
- imported++;
1608
- if (result.embedding)
1609
- vectors++;
1610
- }
1611
- else
1612
- skipped++;
1622
+ if (!result.success)
1623
+ throw new Error(result.error || 'Memory write was rejected');
1624
+ imported++;
1625
+ if (result.embedding)
1626
+ vectors++;
1613
1627
  }
1614
- catch {
1615
- skipped++;
1628
+ catch (error) {
1629
+ failed++;
1630
+ if (errors.length < 10)
1631
+ errors.push(`${e.key}: ${error instanceof Error ? error.message : String(error)}`);
1616
1632
  }
1617
1633
  }
1618
1634
  return {
1635
+ success: failed === 0,
1636
+ error: failed > 0 ? `${failed} memory write(s) failed: ${errors.join('; ')}` : undefined,
1637
+ failed,
1619
1638
  inputPath,
1620
1639
  imported: { entries: imported, vectors, patterns: 0 },
1621
1640
  skipped,
@@ -0,0 +1,3 @@
1
+ /** Only an absent store is empty. Failed reads must never authorize replacement. */
2
+ export declare function readRecordStore<T>(path: string, collection: string, empty: () => T): T;
3
+ //# sourceMappingURL=record-store.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** Only an absent store is empty. Failed reads must never authorize replacement. */
3
+ export function readRecordStore(path, collection, empty) {
4
+ let text;
5
+ try {
6
+ text = readFileSync(path, 'utf-8');
7
+ }
8
+ catch (error) {
9
+ if (error.code === 'ENOENT')
10
+ return empty();
11
+ throw error;
12
+ }
13
+ const store = JSON.parse(text);
14
+ const records = store?.[collection];
15
+ if (!store || typeof store !== 'object' || Array.isArray(store)
16
+ || !records || typeof records !== 'object' || Array.isArray(records)) {
17
+ throw new Error(`Invalid ${collection} store at ${path}; existing data was preserved`);
18
+ }
19
+ return store;
20
+ }
21
+ //# sourceMappingURL=record-store.js.map
@@ -10,6 +10,33 @@ import { join } from 'node:path';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
12
12
  import { validateIdentifier, validateText } from './validate-input.js';
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ // Imported snapshots become live stores on restore. Validate their containers
17
+ // before registering them, while allowing older metadata-only snapshots.
18
+ function validateImportedSession(value) {
19
+ if (!isRecord(value) || (typeof value.name !== 'string' && !isSessionRecordLike(value))) {
20
+ return 'Invalid session: Not a session record; expected a named snapshot or snapshot data';
21
+ }
22
+ if (value.name !== undefined && typeof value.name !== 'string') {
23
+ return 'Invalid session: name must be a string';
24
+ }
25
+ if (value.data === undefined)
26
+ return undefined;
27
+ if (!isRecord(value.data))
28
+ return 'Invalid session: data must be an object';
29
+ for (const [component, collection] of [['tasks', 'tasks'], ['agents', 'agents'], ['memory', 'entries']]) {
30
+ const store = value.data[component];
31
+ if (store === undefined)
32
+ continue;
33
+ if (!isRecord(store) || !isRecord(store[collection]) ||
34
+ Object.values(store[collection]).some(record => !isRecord(record))) {
35
+ return `Invalid session: data.${component}.${collection} must be a record map`;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
13
40
  // Storage paths
14
41
  const STORAGE_DIR = '.claude-flow';
15
42
  const SESSION_DIR = 'sessions';
@@ -306,9 +333,9 @@ export const sessionTools = [
306
333
  properties: {
307
334
  name: { type: 'string', description: 'Session name' },
308
335
  description: { type: 'string', description: 'Session description' },
309
- includeMemory: { type: 'boolean', description: 'Include memory in session' },
310
- includeTasks: { type: 'boolean', description: 'Include tasks in session' },
311
- includeAgents: { type: 'boolean', description: 'Include agents in session' },
336
+ includeMemory: { type: 'boolean', default: true, description: 'Include memory in session' },
337
+ includeTasks: { type: 'boolean', default: true, description: 'Include tasks in session' },
338
+ includeAgents: { type: 'boolean', default: true, description: 'Include agents in session' },
312
339
  },
313
340
  required: ['name'],
314
341
  },
@@ -325,9 +352,9 @@ export const sessionTools = [
325
352
  const sessionId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
326
353
  // Load related data based on options
327
354
  const { data, memoryCapture } = await loadRelatedStores({
328
- includeMemory: input.includeMemory,
329
- includeTasks: input.includeTasks,
330
- includeAgents: input.includeAgents,
355
+ includeMemory: input.includeMemory !== false,
356
+ includeTasks: input.includeTasks !== false,
357
+ includeAgents: input.includeAgents !== false,
331
358
  });
332
359
  // Calculate stats
333
360
  const stats = {
@@ -692,19 +719,26 @@ export const sessionTools = [
692
719
  return { error: `Invalid session JSON: ${e.message}` };
693
720
  }
694
721
  }
695
- if (!isSessionRecordLike(parsed)) {
696
- return { error: 'Not a session record: expected an object produced by session export' };
697
- }
722
+ const validationError = validateImportedSession(parsed);
723
+ if (validationError)
724
+ return { error: validationError };
725
+ const record = parsed;
698
726
  const newId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
699
- const stats = { tasks: 0, agents: 0, memoryEntries: 0, totalSize: 0, ...(parsed.stats || {}) };
727
+ const stats = {
728
+ tasks: Object.keys(record.data?.tasks?.tasks || {}).length,
729
+ agents: Object.keys(record.data?.agents?.agents || {}).length,
730
+ memoryEntries: countMemoryEntries(record.data?.memory),
731
+ totalSize: 0,
732
+ };
700
733
  const session = {
701
734
  sessionId: newId,
702
- name: input.name ? String(input.name) : (parsed.name || 'imported-session'),
703
- description: parsed.description,
735
+ name: input.name ? String(input.name) : (record.name || 'imported-session'),
736
+ description: record.description,
704
737
  savedAt: new Date().toISOString(),
705
738
  stats,
706
- data: parsed.data,
739
+ data: record.data,
707
740
  };
741
+ session.stats.totalSize = Buffer.byteLength(JSON.stringify(session), 'utf-8');
708
742
  saveSession(session);
709
743
  let activated = false;
710
744
  if (input.activate === true) {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ import { readRecordStore } from './record-store.js';
8
9
  import { getProjectCwd } from './types.js';
9
10
  import { validateIdentifier, validateText } from './validate-input.js';
10
11
  // Storage paths
@@ -24,17 +25,7 @@ function ensureTaskDir() {
24
25
  }
25
26
  }
26
27
  function loadTaskStore() {
27
- try {
28
- const path = getTaskPath();
29
- if (existsSync(path)) {
30
- const data = readFileSync(path, 'utf-8');
31
- return JSON.parse(data);
32
- }
33
- }
34
- catch {
35
- // Return empty store on error
36
- }
37
- return { tasks: {}, version: '3.0.0' };
28
+ return readRecordStore(getTaskPath(), 'tasks', () => ({ tasks: {}, version: '3.0.0' }));
38
29
  }
39
30
  function saveTaskStore(store) {
40
31
  ensureTaskDir();
@@ -378,6 +369,9 @@ export const taskTools = [
378
369
  return { taskId, error: 'Task not found' };
379
370
  }
380
371
  const previouslyAssigned = [...task.assignedTo];
372
+ // Load agent store to sync worker state
373
+ const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json');
374
+ readRecordStore(agentStorePath, 'agents', () => ({ agents: {} }));
381
375
  if (input.unassign) {
382
376
  // Revert previously assigned agents to idle
383
377
  releaseAgents(previouslyAssigned, taskId);
@@ -41,6 +41,22 @@ function saveWorkflowStore(store) {
41
41
  ensureWorkflowDir();
42
42
  writeFileSync(getWorkflowPath(), JSON.stringify(store, null, 2), 'utf-8');
43
43
  }
44
+ // A paused in-flight step still owns its executor. Resume joins that execution
45
+ // instead of dispatching the same side effect again (within this process).
46
+ const workflowExecutions = new Map();
47
+ const workflowExecutionKey = (workflowId) => JSON.stringify([getWorkflowPath(), workflowId]);
48
+ function withWorkflowExecution(workflowId, execute) {
49
+ const key = workflowExecutionKey(workflowId);
50
+ if (workflowExecutions.has(key)) {
51
+ return Promise.resolve({ workflowId, error: 'Workflow already executing' });
52
+ }
53
+ const execution = execute();
54
+ workflowExecutions.set(key, execution);
55
+ return execution.finally(() => {
56
+ if (workflowExecutions.get(key) === execution)
57
+ workflowExecutions.delete(key);
58
+ });
59
+ }
44
60
  export const workflowTools = [
45
61
  {
46
62
  name: 'workflow_run',
@@ -227,7 +243,7 @@ export const workflowTools = [
227
243
  },
228
244
  required: ['workflowId'],
229
245
  },
230
- handler: async (input) => {
246
+ handler: async (input) => withWorkflowExecution(input.workflowId, async () => {
231
247
  // Validate user-provided input (#1425)
232
248
  const vId = validateIdentifier(input.workflowId, 'workflowId');
233
249
  if (!vId.valid)
@@ -241,13 +257,50 @@ export const workflowTools = [
241
257
  if (workflow.status === 'running') {
242
258
  return { workflowId, error: 'Workflow already running' };
243
259
  }
260
+ // Indices may name a step or the end boundary (used when resuming
261
+ // after the final step). Reject invalid control flow before any writes.
262
+ const validIndex = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= workflow.steps.length;
263
+ const startFromStep = input.startFromStep === undefined ? 0 : input.startFromStep;
264
+ if (!validIndex(startFromStep)) {
265
+ return { workflowId, error: `startFromStep must be an integer between 0 and ${workflow.steps.length}` };
266
+ }
267
+ for (const step of workflow.steps) {
268
+ if (step.type !== 'condition')
269
+ continue;
270
+ for (const target of ['thenStep', 'elseStep']) {
271
+ if (step.config[target] !== undefined && !validIndex(step.config[target])) {
272
+ return { workflowId, error: `${step.stepId}.${target} must be an integer between 0 and ${workflow.steps.length}` };
273
+ }
274
+ }
275
+ }
276
+ const persistProgress = () => {
277
+ // An awaited step may overlap pause/stop/delete or a sibling workflow
278
+ // mutation. Merge only this workflow into the latest store snapshot.
279
+ const latest = loadWorkflowStore();
280
+ const current = latest.workflows[workflowId];
281
+ if (!current) {
282
+ workflow.status = 'failed';
283
+ workflow.error = 'Workflow was deleted during execution';
284
+ return;
285
+ }
286
+ if (current.status === 'failed') {
287
+ Object.assign(workflow, current);
288
+ return;
289
+ }
290
+ if (current.status === 'paused' && workflow.status !== 'failed')
291
+ workflow.status = 'paused';
292
+ latest.workflows[workflowId] = workflow;
293
+ saveWorkflowStore(latest);
294
+ };
244
295
  // Inject runtime variables
245
296
  if (input.variables) {
246
297
  workflow.variables = { ...workflow.variables, ...input.variables };
247
298
  }
248
299
  workflow.status = 'running';
249
300
  workflow.startedAt = new Date().toISOString();
250
- workflow.currentStep = input.startFromStep || 0;
301
+ delete workflow.error;
302
+ delete workflow.completedAt;
303
+ workflow.currentStep = startFromStep;
251
304
  saveWorkflowStore(store);
252
305
  // ADR-095 G3: real workflow runtime. Walk the steps in order;
253
306
  // dispatch each by type. Persist progress after each step so a
@@ -277,22 +330,26 @@ export const workflowTools = [
277
330
  while (i < workflow.steps.length) {
278
331
  // Honor pause/cancel signals between steps.
279
332
  const live = loadWorkflowStore().workflows[workflowId];
280
- if (!live || live.status === 'paused') {
333
+ if (!live) {
334
+ workflow.status = 'failed';
335
+ workflow.error = 'Workflow was deleted during execution';
336
+ break;
337
+ }
338
+ if (live.status === 'paused') {
281
339
  workflow.status = 'paused';
282
340
  workflow.currentStep = i;
283
- saveWorkflowStore(store);
341
+ persistProgress();
284
342
  break;
285
343
  }
286
344
  if (live.status === 'failed') {
287
- workflow.status = 'failed';
288
- saveWorkflowStore(store);
345
+ Object.assign(workflow, live);
289
346
  break;
290
347
  }
291
348
  const step = workflow.steps[i];
292
349
  step.status = 'running';
293
350
  step.startedAt = new Date().toISOString();
294
351
  const stepStart = Date.now();
295
- saveWorkflowStore(store);
352
+ persistProgress();
296
353
  let stepEntry = { stepId: step.stepId, type: step.type, status: 'running' };
297
354
  try {
298
355
  if (step.type === 'task') {
@@ -364,7 +421,7 @@ export const workflowTools = [
364
421
  workflow.status = 'failed';
365
422
  workflow.error = msg;
366
423
  workflow.completedAt = new Date().toISOString();
367
- saveWorkflowStore(store);
424
+ persistProgress();
368
425
  return {
369
426
  workflowId,
370
427
  status: 'failed',
@@ -379,17 +436,18 @@ export const workflowTools = [
379
436
  stepEntry.durationMs = Date.now() - stepStart;
380
437
  stepResults.push(stepEntry);
381
438
  workflow.currentStep = i + 1;
382
- saveWorkflowStore(store);
439
+ persistProgress();
383
440
  i++;
384
441
  }
385
442
  if (workflow.status === 'running') {
386
443
  workflow.status = 'completed';
387
444
  workflow.completedAt = new Date().toISOString();
388
- saveWorkflowStore(store);
445
+ persistProgress();
389
446
  }
390
447
  return {
391
448
  workflowId,
392
449
  status: workflow.status,
450
+ ...(workflow.error ? { error: workflow.error } : {}),
393
451
  totalSteps: workflow.steps.length,
394
452
  stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
395
453
  stepsSkipped: stepResults.filter(s => s.status === 'skipped').length,
@@ -399,7 +457,7 @@ export const workflowTools = [
399
457
  completedAt: workflow.completedAt,
400
458
  durationMs: Date.now() - startedAt,
401
459
  };
402
- },
460
+ }),
403
461
  },
404
462
  {
405
463
  name: 'workflow_status',
@@ -560,24 +618,15 @@ export const workflowTools = [
560
618
  if (workflow.status !== 'paused') {
561
619
  return { workflowId, error: 'Workflow not paused' };
562
620
  }
563
- workflow.status = 'running';
564
- saveWorkflowStore(store);
565
- // Report current step states — do not auto-complete them
566
- const stepStates = workflow.steps.map(step => ({
567
- stepId: step.stepId,
568
- name: step.name,
569
- status: step.status,
570
- }));
571
- const remainingSteps = workflow.steps.length - workflow.currentStep;
572
- return {
573
- workflowId,
574
- status: workflow.status,
575
- resumed: true,
576
- currentStep: workflow.currentStep,
577
- remainingSteps,
578
- steps: stepStates,
579
- _note: 'Workflow resumed. Steps remain in their current state and must be executed via task tools.',
580
- };
621
+ const active = workflowExecutions.get(workflowExecutionKey(workflowId));
622
+ if (active) {
623
+ workflow.status = 'running';
624
+ saveWorkflowStore(store);
625
+ return { ...(await active), resumed: true };
626
+ }
627
+ const execute = workflowTools.find(tool => tool.name === 'workflow_execute');
628
+ const result = await execute.handler({ workflowId, startFromStep: workflow.currentStep });
629
+ return { ...result, resumed: true };
581
630
  },
582
631
  },
583
632
  {
@@ -0,0 +1,28 @@
1
+ export type AppendCondition = {
2
+ namespace: string;
3
+ } & ({
4
+ key: string;
5
+ sha256: string;
6
+ latestPrefix?: string;
7
+ } | {
8
+ absent: true;
9
+ latestPrefix: string;
10
+ } | {
11
+ keysSha256: string;
12
+ count: number;
13
+ });
14
+ export declare class AppendConditionFailed extends Error {
15
+ }
16
+ export declare function validateAppendConditions(input: unknown): AppendCondition[];
17
+ interface NativeReader {
18
+ prepare(sql: string): {
19
+ all(...parameters: string[]): Array<{
20
+ key: string;
21
+ content: string;
22
+ status: string | null;
23
+ }>;
24
+ };
25
+ }
26
+ export declare function assertAppendConditions(db: NativeReader, conditions: AppendCondition[]): void;
27
+ export {};
28
+ //# sourceMappingURL=append-conditions.d.ts.map
@@ -0,0 +1,56 @@
1
+ /** Native append predicates checked in the SAME write transaction as an immutable insert. */
2
+ import { createHash } from 'node:crypto';
3
+ export class AppendConditionFailed extends Error {
4
+ }
5
+ const text = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
6
+ const digest = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
7
+ export function validateAppendConditions(input) {
8
+ if (!Array.isArray(input) || input.length < 1 || input.length > 8)
9
+ throw new TypeError('append conditions must contain 1-8 predicates');
10
+ return input.map(condition => {
11
+ if (!condition || typeof condition !== 'object' || Array.isArray(condition))
12
+ throw new TypeError('invalid append condition');
13
+ const row = condition;
14
+ const fields = Object.keys(row).sort().join(',');
15
+ const validRecord = ['key,namespace,sha256', 'key,latestPrefix,namespace,sha256'].includes(fields)
16
+ && text(row.key, 1024) && digest(row.sha256) && (row.latestPrefix === undefined || text(row.latestPrefix, 1024));
17
+ const validAbsent = fields === 'absent,latestPrefix,namespace' && row.absent === true && text(row.latestPrefix, 1024);
18
+ const validKeys = fields === 'count,keysSha256,namespace' && digest(row.keysSha256)
19
+ && Number.isSafeInteger(row.count) && row.count >= 0 && row.count <= 100_000;
20
+ if (!text(row.namespace, 256) || !(validRecord || validAbsent || validKeys))
21
+ throw new TypeError('invalid append condition');
22
+ return row;
23
+ });
24
+ }
25
+ const sha = (value) => createHash('sha256').update(value).digest('hex');
26
+ export function assertAppendConditions(db, conditions) {
27
+ for (const condition of conditions) {
28
+ if ('keysSha256' in condition) {
29
+ const rows = db.prepare("SELECT key FROM memory_entries WHERE namespace = ? AND (status = 'active' OR status IS NULL)")
30
+ .all(condition.namespace);
31
+ const keys = rows.map(row => row.key).sort();
32
+ if (keys.length !== condition.count || sha(JSON.stringify(keys)) !== condition.keysSha256) {
33
+ throw new AppendConditionFailed('immutable append rejected: namespace membership changed');
34
+ }
35
+ continue;
36
+ }
37
+ if ('key' in condition) {
38
+ const rows = db.prepare('SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key = ?')
39
+ .all(condition.namespace, condition.key);
40
+ if (rows.length !== 1 || !['active', null].includes(rows[0].status)
41
+ || typeof rows[0].content !== 'string' || sha(rows[0].content) !== condition.sha256) {
42
+ throw new AppendConditionFailed('immutable append rejected: expected parent/authority bytes changed or absent');
43
+ }
44
+ }
45
+ if (condition.latestPrefix !== undefined) {
46
+ // Include tombstones: deleting a new authority slot must never rewind to an older authority.
47
+ const pattern = condition.latestPrefix.replace(/[\\%_]/g, character => `\\${character}`) + '%';
48
+ const latest = db.prepare("SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key LIKE ? ESCAPE '\\' ORDER BY key COLLATE BINARY DESC LIMIT 1")
49
+ .all(condition.namespace, pattern);
50
+ if ('absent' in condition ? latest.length !== 0 : latest.length !== 1 || latest[0].key !== condition.key) {
51
+ throw new AppendConditionFailed('immutable append rejected: newer parent/authority slot exists');
52
+ }
53
+ }
54
+ }
55
+ }
56
+ //# sourceMappingURL=append-conditions.js.map
@@ -29,6 +29,15 @@
29
29
  *
30
30
  * @module v3/cli/memory/graph-edge-writer
31
31
  */
32
+ /**
33
+ * True iff the memory.db bridge at `customDbPath` (or the default location)
34
+ * is currently RFE1-encrypted-at-rest. Callers that receive `null` from
35
+ * getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
36
+ * destructive `memory init` command for this specific, non-corrupt case.
37
+ */
38
+ export declare function isBridgeDbEncryptedAtRest(customDbPath?: string): boolean;
39
+ /** Test-only: clear the once-per-path warning dedup set. */
40
+ export declare function _resetRfe1WarningFlag(): void;
32
41
  /**
33
42
  * Checkpoint and close the cached handle if it is open (optionally only if it
34
43
  * is open on `dbPath`). Returns true if a handle was released. Never throws.