@claude-flow/cli 3.34.0 → 3.35.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 (35) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/.helpers-version +1 -1
  3. package/.claude/helpers/helpers.manifest.json +2 -2
  4. package/.claude/helpers/statusline.cjs +0 -0
  5. package/.claude/proven-config.json +42 -0
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/daemon.js +12 -7
  8. package/dist/src/commands/doctor.js +72 -1
  9. package/dist/src/commands/metaharness.js +37 -2
  10. package/dist/src/log-filters.d.ts +3 -3
  11. package/dist/src/mcp-tools/metaharness-tools.js +35 -2
  12. package/dist/src/memory/memory-bridge.d.ts +25 -0
  13. package/dist/src/memory/memory-bridge.js +61 -8
  14. package/dist/src/memory/memory-initializer.d.ts +9 -3
  15. package/dist/src/memory/memory-initializer.js +344 -277
  16. package/dist/src/services/daemon-autostart.d.ts +31 -3
  17. package/dist/src/services/daemon-autostart.js +45 -3
  18. package/dist/src/services/distill-oracle.d.ts +1 -1
  19. package/dist/src/services/distill-oracle.js +2 -2
  20. package/dist/src/services/evolve-proof.d.ts +40 -1
  21. package/dist/src/services/evolve-proof.js +76 -14
  22. package/dist/src/services/flywheel-receipt.d.ts +15 -0
  23. package/dist/src/services/flywheel-receipt.js +22 -0
  24. package/dist/src/services/flywheel-sequential-evidence.d.ts +102 -0
  25. package/dist/src/services/flywheel-sequential-evidence.js +148 -0
  26. package/dist/src/services/flywheel-transaction.d.ts +71 -0
  27. package/dist/src/services/flywheel-transaction.js +121 -0
  28. package/dist/src/services/harness-flywheel-generations.d.ts +14 -0
  29. package/dist/src/services/harness-flywheel-generations.js +76 -4
  30. package/dist/src/services/harness-flywheel.d.ts +13 -0
  31. package/dist/src/services/harness-flywheel.js +31 -1
  32. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  33. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  34. package/package.json +4 -9
  35. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -2
@@ -0,0 +1 @@
1
+ sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319
@@ -1 +1 @@
1
- 3.32.29
1
+ 3.35.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.34.0",
3
+ "version": "3.35.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
6
6
  "hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
@@ -8,6 +8,6 @@
8
8
  "statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
9
9
  }
10
10
  },
11
- "signature": "GyTvYvhL7vIta/4yyz1rIyl+DWJ+x4dJVfmcrKZJXlh4E/TcQK2bszRznlaG1o7LWSZTudaYj50w+/7dlv6lDg==",
11
+ "signature": "tnK2I0enYGI5ql364QJqnAUHMo+pIh0AZK4y/Rad9H+WuPo/KzzG4jqL8/083EbEKWLPEneN6n9s0pEQcGzVCw==",
12
12
  "algorithm": "ed25519"
13
13
  }
File without changes
@@ -0,0 +1,42 @@
1
+ {
2
+ "adoptedAt": 1785208363552,
3
+ "championId": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
4
+ "manifest": {
5
+ "schema": "ruflo.proven-config/v1",
6
+ "policy": {
7
+ "ref": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
8
+ "value": {
9
+ "alpha": 0.3,
10
+ "subjectWeight": 1,
11
+ "mmrLambda": 0.5,
12
+ "bodyWeight": 1.5,
13
+ "typePenaltyFactor": 0.5
14
+ }
15
+ },
16
+ "layer": "framework/node-cli",
17
+ "compatibility": {
18
+ "ruflo": ">=3.24.0"
19
+ },
20
+ "benchmark": {
21
+ "corpus": "ADR-081-labelled-v1",
22
+ "corpusHash": "sha256:2f700b5c363e20a3bd88ce2bc9b87bbbbaa61732c6177894c6ec37890f888982"
23
+ },
24
+ "receipt": {
25
+ "heldOutDelta": 0.07381404928570845,
26
+ "redblue": "PASS",
27
+ "drift": 0,
28
+ "canary": {
29
+ "rollbackRate": 0,
30
+ "latencyP95": 244.612458000076,
31
+ "costPerTask": 0
32
+ },
33
+ "receiptCoverage": 1
34
+ },
35
+ "platform": [
36
+ "linux",
37
+ "macOS",
38
+ "windows"
39
+ ]
40
+ },
41
+ "previous": ""
42
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 4,
4
- "generatedAt": "2026-07-31T15:55:49.000Z",
5
- "gitSha": "4ac1ab9f",
4
+ "generatedAt": "2026-08-10T14:19:13.896Z",
5
+ "gitSha": "4af1cfdd",
6
6
  "catalog": {
7
7
  "agents": 164,
8
8
  "tools": 397,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { output } from '../output.js';
6
6
  import { getDaemon, startDaemon, stopDaemon } from '../services/worker-daemon.js';
7
+ import { resolveDaemonProjectRoot } from '../services/daemon-autostart.js';
7
8
  import { fork } from 'child_process';
8
9
  import { fileURLToPath } from 'url';
9
10
  import { dirname, join, resolve } from 'path';
@@ -61,7 +62,11 @@ const startCommand = {
61
62
  }
62
63
  // #1914: a forked daemon child receives --workspace <root>; the launcher
63
64
  // and interactive invocations have no flag and fall back to cwd.
64
- const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace) ?? process.cwd();
65
+ // #2877: that cwd fallback is normalized to the owning project root, so
66
+ // `daemon start` from a subdirectory contends for the SAME lock/PID file
67
+ // as one from the root instead of silently keying its own pair.
68
+ const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace)
69
+ ?? resolveDaemonProjectRoot(process.cwd());
65
70
  const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
66
71
  // Parse resource threshold overrides from CLI flags
67
72
  const config = {};
@@ -605,7 +610,7 @@ const stopCommand = {
605
610
  ],
606
611
  action: async (ctx) => {
607
612
  const quiet = ctx.flags.quiet;
608
- const projectRoot = process.cwd();
613
+ const projectRoot = resolveDaemonProjectRoot(process.cwd());
609
614
  // #2661: `stop --all` — the containment lever for daemon fanout across
610
615
  // Git worktrees. Only processes positively identified as ruflo daemons
611
616
  // (via their self-identifying argv) are touched; each receives SIGTERM
@@ -657,7 +662,7 @@ async function stopAllDaemons(quiet) {
657
662
  await stopDaemon();
658
663
  }
659
664
  catch { /* not running in-process */ }
660
- await killBackgroundDaemon(process.cwd());
665
+ await killBackgroundDaemon(resolveDaemonProjectRoot(process.cwd()));
661
666
  const daemons = await scanRunningDaemons();
662
667
  if (daemons.length === 0) {
663
668
  if (!quiet) {
@@ -1160,7 +1165,7 @@ const statusCommand = {
1160
1165
  if (ctx.flags.all) {
1161
1166
  return renderAllDaemonsStatus();
1162
1167
  }
1163
- const projectRoot = process.cwd();
1168
+ const projectRoot = resolveDaemonProjectRoot(process.cwd());
1164
1169
  try {
1165
1170
  const daemon = getDaemon(projectRoot);
1166
1171
  const status = daemon.getStatus();
@@ -1304,7 +1309,7 @@ const triggerCommand = {
1304
1309
  // #2661: an explicit `trigger --headless` is user consent for AI
1305
1310
  // execution of THIS run (still governed by the global AI budget).
1306
1311
  // Without the flag, config.json / env opt-in still applies.
1307
- const daemon = getDaemon(process.cwd(), ctx.flags.headless === true ? { aiWorkersEnabled: true } : undefined);
1312
+ const daemon = getDaemon(resolveDaemonProjectRoot(process.cwd()), ctx.flags.headless === true ? { aiWorkersEnabled: true } : undefined);
1308
1313
  const spinner = output.createSpinner({ text: `Running ${workerType} worker...`, spinner: 'dots' });
1309
1314
  spinner.start();
1310
1315
  const result = await daemon.triggerWorker(workerType);
@@ -1347,7 +1352,7 @@ const enableCommand = {
1347
1352
  return { success: false, exitCode: 1 };
1348
1353
  }
1349
1354
  try {
1350
- const daemon = getDaemon(process.cwd());
1355
+ const daemon = getDaemon(resolveDaemonProjectRoot(process.cwd()));
1351
1356
  daemon.setWorkerEnabled(workerType, !disable);
1352
1357
  output.printSuccess(`Worker ${workerType} ${disable ? 'disabled' : 'enabled'}`);
1353
1358
  return { success: true };
@@ -1402,7 +1407,7 @@ const installSupervisorCommand = {
1402
1407
  const force = ctx.flags.force === true;
1403
1408
  const load = ctx.flags.load !== false;
1404
1409
  const dryRun = ctx.flags['dry-run'] === true || ctx.flags.dryRun === true;
1405
- const projectRoot = process.cwd();
1410
+ const projectRoot = resolveDaemonProjectRoot(process.cwd());
1406
1411
  const platform = process.platform;
1407
1412
  if (platform === 'win32') {
1408
1413
  output.printError('Windows scheduled-task installer is not yet implemented.');
@@ -1766,6 +1766,76 @@ async function checkMetaharnessIntegration() {
1766
1766
  };
1767
1767
  }
1768
1768
  }
1769
+ /**
1770
+ * Dependency-contract check: every `@metaharness/*` package this CLI DECLARES
1771
+ * in its own optionalDependencies must actually resolve at runtime. Declared
1772
+ * packages are advertised integration surfaces (`ruflo metaharness evolve`,
1773
+ * flywheel receipt interop, radio coordination) — a declared-but-absent
1774
+ * package means the install dropped an optional dep (or the declaration
1775
+ * regressed to peer-only), so the advertised integration silently degrades.
1776
+ * That is a FAIL, not a warn: warn is reserved for surfaces that were never
1777
+ * advertised as installed (the `npx metaharness` umbrella path above).
1778
+ */
1779
+ async function checkMetaharnessDeclaredPackages() {
1780
+ const NAME = 'MetaHarness declared packages (ADR-150)';
1781
+ try {
1782
+ // Walk up from this module to the CLI package root (works for npx cache,
1783
+ // global install, project-local install, and monorepo dev alike).
1784
+ let root = null;
1785
+ let q = dirname(fileURLToPath(import.meta.url));
1786
+ for (let i = 0; i < 8; i++) {
1787
+ const pj = join(q, 'package.json');
1788
+ if (existsSync(pj)) {
1789
+ try {
1790
+ if (JSON.parse(readFileSync(pj, 'utf-8')).name === '@claude-flow/cli') {
1791
+ root = q;
1792
+ break;
1793
+ }
1794
+ }
1795
+ catch { /* keep walking */ }
1796
+ }
1797
+ q = dirname(q);
1798
+ }
1799
+ if (!root)
1800
+ return { name: NAME, status: 'warn', message: 'could not locate the @claude-flow/cli package root' };
1801
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
1802
+ const declared = Object.keys(pkg.optionalDependencies ?? {}).filter((n) => n === 'metaharness' || n.startsWith('@metaharness/'));
1803
+ if (declared.length === 0) {
1804
+ return {
1805
+ name: NAME,
1806
+ status: 'fail',
1807
+ message: 'no @metaharness/* packages declared in optionalDependencies — the dependency contract regressed (peer-only declarations are never installed)',
1808
+ fix: 'Restore @metaharness/darwin, @metaharness/flywheel, @metaharness/radio to optionalDependencies in @claude-flow/cli',
1809
+ };
1810
+ }
1811
+ // Node resolution: nearest node_modules wins, walking upward from the CLI root.
1812
+ const resolves = (name) => {
1813
+ let d = root;
1814
+ for (let i = 0; i < 10; i++) {
1815
+ if (existsSync(join(d, 'node_modules', name, 'package.json')))
1816
+ return true;
1817
+ const parent = dirname(d);
1818
+ if (parent === d)
1819
+ break;
1820
+ d = parent;
1821
+ }
1822
+ return false;
1823
+ };
1824
+ const missing = declared.filter((n) => !resolves(n));
1825
+ if (missing.length > 0) {
1826
+ return {
1827
+ name: NAME,
1828
+ status: 'fail',
1829
+ message: `declared but not installed: ${missing.join(', ')} — advertised MetaHarness surfaces are degraded`,
1830
+ fix: 'npm install --include=optional # optional deps were skipped or pruned',
1831
+ };
1832
+ }
1833
+ return { name: NAME, status: 'pass', message: `${declared.length} declared package(s) resolve: ${declared.join(', ')}` };
1834
+ }
1835
+ catch (err) {
1836
+ return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1837
+ }
1838
+ }
1769
1839
  async function checkMetaharness() {
1770
1840
  try {
1771
1841
  const version = await runCommand('npx -y metaharness@latest --version 2>&1', 15000);
@@ -2116,6 +2186,7 @@ export const doctorCommand = {
2116
2186
  checkEncryptionAtRest, // ADR-096 Phase 5
2117
2187
  checkFederationBreaker, // ADR-097 Phase 4
2118
2188
  checkMetaharness, // ADR-150 — MetaHarness upstream package
2189
+ checkMetaharnessDeclaredPackages, // dependency contract — declared optional deps must resolve
2119
2190
  checkMetaharnessIntegration, // iter 45 — ruflo-side integration layer
2120
2191
  checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
2121
2192
  checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
@@ -2158,7 +2229,7 @@ export const doctorCommand = {
2158
2229
  'agentic-flow': checkAgenticFlow,
2159
2230
  'encryption': checkEncryptionAtRest, // ADR-096 Phase 5
2160
2231
  'federation': checkFederationBreaker, // ADR-097 Phase 4
2161
- 'metaharness': checkMetaharness, // ADR-150 — upstream package
2232
+ 'metaharness': [checkMetaharness, checkMetaharnessDeclaredPackages, checkMetaharnessIntegration], // ADR-150 — upstream + declared deps + ruflo-side
2162
2233
  'metaharness-integration': checkMetaharnessIntegration, // iter 45 — ruflo-side
2163
2234
  'funnel': checkFunnel, // ADR-305
2164
2235
  // ADR-307 — deep-dive array, same pattern as 'memory' above: the cheap
@@ -39,7 +39,7 @@ import { existsSync, readFileSync } from 'fs';
39
39
  import { join, dirname, resolve } from 'path';
40
40
  import { fileURLToPath } from 'url';
41
41
  import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
42
- import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
42
+ import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, resetSequentialEvidence, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
43
43
  import { evaluatePolicyRequest } from '../services/policy-runtime.js';
44
44
  const __dirname = dirname(fileURLToPath(import.meta.url));
45
45
  // Subcommand → plugin script filename
@@ -124,6 +124,37 @@ async function dispatchFlywheel(operation, positional, flags) {
124
124
  const state = readFlywheelTransactionState(projectRoot);
125
125
  data = { ledgerHead: state.ledgerHead, commits: state.commits };
126
126
  }
127
+ else if (operation === 'evidence-reset') {
128
+ // ADR-381 §2 — start a new evidence epoch. Same policy gate as promote:
129
+ // this reopens the family-wise alpha budget, so it is a governance action.
130
+ const reason = String(flywheelFlag(flags, 'reason', '')).trim();
131
+ if (!reason) {
132
+ data = { success: false, reason: 'evidence-reset requires --reason "<why>" — the reset audit trail records intent' };
133
+ }
134
+ else {
135
+ const policy = await evaluatePolicyRequest({
136
+ identity: { id: process.env.CLAUDE_FLOW_PRINCIPAL_ID ?? 'metaharness-local', type: 'agent', roles: ['optimizer'] },
137
+ action: {
138
+ type: 'metaharness.evidence.reset',
139
+ resource: projectRoot,
140
+ environment: 'production',
141
+ destructive: true,
142
+ },
143
+ context: {
144
+ approvalIds: String(flywheelFlag(flags, 'approvalId', '')).split(',').filter(Boolean),
145
+ },
146
+ }, projectRoot);
147
+ if (policy.enforcedOutcome !== 'allowed') {
148
+ data = { success: false, reason: `policy-${policy.enforcedOutcome}:${policy.reason}`, policyReceiptId: policy.receiptId };
149
+ }
150
+ else {
151
+ data = await resetSequentialEvidence(projectRoot, {
152
+ confirm: flywheelFlag(flags, 'confirm', false) === true,
153
+ reason,
154
+ });
155
+ }
156
+ }
157
+ }
127
158
  else if (operation === 'promote') {
128
159
  const receiptId = positional[0];
129
160
  const publicKeyPath = flywheelFlag(flags, 'publicKey');
@@ -151,6 +182,10 @@ async function dispatchFlywheel(operation, positional, flags) {
151
182
  data = await promoteFlywheelCandidate(projectRoot, receiptId, {
152
183
  confirm: flywheelFlag(flags, 'confirm', false) === true,
153
184
  trustedPublicKeys: new Set([publicKey]),
185
+ // --allow-aggregate-evidence: explicit migration escape hatch for
186
+ // pre-upgrade receipts without task-level pairedOutcomes. Default
187
+ // is strict — aggregate-only evidence is refused.
188
+ requirePairedEvidence: flywheelFlag(flags, 'allowAggregateEvidence', false) !== true,
154
189
  });
155
190
  }
156
191
  }
@@ -288,7 +323,7 @@ export const metaharnessCommand = {
288
323
  output.writeln(' redblue adversarial red/blue LLM testing (init|run|patch|attack|report)');
289
324
  output.writeln(' evolve Darwin candidate evolution');
290
325
  output.writeln(' bench create or verify a stable benchmark suite');
291
- output.writeln(' flywheel receipt loop: run | status | receipts | history | promote');
326
+ output.writeln(' flywheel receipt loop: run | status | receipts | history | promote | evidence-reset');
292
327
  output.writeln('');
293
328
  output.writeln('Each subcommand accepts --format json|table and --help.');
294
329
  output.writeln('');
@@ -40,7 +40,7 @@ declare const STDERR_REDIRECT_PREFIXES: string[];
40
40
  declare const AGENTDB_MOCK_FALLBACK_DROP_PREFIXES: string[];
41
41
  declare const shouldRedirectToStderr: (msg: unknown) => boolean;
42
42
  declare const isAgentdbMockFallbackNoise: (msg: unknown) => boolean;
43
- declare const origWarn: (message?: any, ...optionalParams: any[]) => void;
44
- declare const origLog: (message?: any, ...optionalParams: any[]) => void;
45
- declare const origError: (message?: any, ...optionalParams: any[]) => void;
43
+ declare const origWarn: (...data: any[]) => void;
44
+ declare const origLog: (...data: any[]) => void;
45
+ declare const origError: (...data: any[]) => void;
46
46
  //# sourceMappingURL=log-filters.d.ts.map
@@ -53,7 +53,7 @@ import { join, dirname, resolve } from 'node:path';
53
53
  import { fileURLToPath } from 'node:url';
54
54
  import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
55
55
  import { evaluatePolicyRequest } from '../services/policy-runtime.js';
56
- import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
56
+ import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, resetSequentialEvidence, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
57
57
  const __dirname = dirname(fileURLToPath(import.meta.url));
58
58
  /**
59
59
  * Walk up from this module to find plugins/ruflo-metaharness/scripts/.
@@ -690,7 +690,7 @@ export const metaharnessTools = [
690
690
  inputSchema: {
691
691
  type: 'object',
692
692
  properties: {
693
- operation: { type: 'string', enum: ['run', 'status', 'receipts', 'history', 'promote'], description: 'Flywheel operation', default: 'status' },
693
+ operation: { type: 'string', enum: ['run', 'status', 'receipts', 'history', 'promote', 'evidence-reset'], description: 'Flywheel operation', default: 'status' },
694
694
  projectRoot: { type: 'string', description: 'Target project root (default: active project cwd)' },
695
695
  receiptId: { type: 'string', description: 'Receipt content ID for promote' },
696
696
  sample: { type: 'number', description: 'Maximum harvested evaluation sample', default: 40 },
@@ -704,6 +704,8 @@ export const metaharnessTools = [
704
704
  anchorHash: { type: 'string', description: 'Pinned sha256 of canonical anchor tasks; requires anchorPath' },
705
705
  anchorManifestPath: { type: 'string', description: 'Project-contained anchor manifest path (default .claude/eval/flywheel-anchor.manifest.json)' },
706
706
  approvalIds: { type: 'array', items: { type: 'string' }, description: 'Scoped ADR-324 approval IDs for privileged promotion' },
707
+ allowAggregateEvidence: { type: 'boolean', description: 'Migration escape hatch: accept a pre-upgrade receipt without task-level pairedOutcomes. Default false — aggregate-only evidence is refused by the strict sequential-evidence gate.', default: false },
708
+ reason: { type: 'string', description: 'Required for evidence-reset (ADR-381): the human intent recorded in the append-only reset audit trail.' },
707
709
  },
708
710
  required: ['operation'],
709
711
  },
@@ -758,6 +760,36 @@ export const metaharnessTools = [
758
760
  });
759
761
  return { success: data.ran, data, degraded: false, exitCode: data.ran ? 0 : 1 };
760
762
  }
763
+ if (operation === 'evidence-reset') {
764
+ // ADR-381 §2 — reopens the family-wise alpha budget; same policy
765
+ // gate as promote, plus a mandatory recorded reason.
766
+ const reason = String(input.reason ?? '').trim();
767
+ if (!reason) {
768
+ return { success: false, data: { reason: 'evidence-reset requires a non-empty reason — the reset audit trail records intent' }, degraded: false, exitCode: 2 };
769
+ }
770
+ const policy = await evaluatePolicyRequest({
771
+ identity: { id: 'metaharness-mcp', type: 'agent', roles: ['optimizer'] },
772
+ action: {
773
+ type: 'metaharness.evidence.reset',
774
+ resource: projectRoot,
775
+ environment: 'production',
776
+ destructive: true,
777
+ },
778
+ context: {
779
+ approvalIds: Array.isArray(input.approvalIds) ? input.approvalIds.map(String) : undefined,
780
+ },
781
+ }, projectRoot);
782
+ if (policy.enforcedOutcome !== 'allowed') {
783
+ return {
784
+ success: false,
785
+ data: { reason: `policy-${policy.enforcedOutcome}:${policy.reason}`, policyReceiptId: policy.receiptId },
786
+ degraded: false,
787
+ exitCode: 1,
788
+ };
789
+ }
790
+ const data = await resetSequentialEvidence(projectRoot, { confirm: input.confirm === true, reason });
791
+ return { success: data.success, data, degraded: false, exitCode: data.success ? 0 : 1 };
792
+ }
761
793
  if (operation === 'promote') {
762
794
  if (!input.receiptId || !input.publicKeyPath) {
763
795
  return { success: false, data: { reason: 'receiptId and publicKeyPath are required' }, degraded: false, exitCode: 2 };
@@ -786,6 +818,7 @@ export const metaharnessTools = [
786
818
  const data = await promoteFlywheelCandidate(projectRoot, String(input.receiptId), {
787
819
  confirm: input.confirm === true,
788
820
  trustedPublicKeys: new Set([publicKey]),
821
+ requirePairedEvidence: input.allowAggregateEvidence !== true,
789
822
  });
790
823
  return { success: data.success, data, degraded: false, exitCode: data.success ? 0 : 1 };
791
824
  }
@@ -487,6 +487,14 @@ export declare function bridgeHealthCheck(dbPath?: string): Promise<{
487
487
  hits: number;
488
488
  misses: number;
489
489
  };
490
+ hierarchicalMemory?: {
491
+ controller: string;
492
+ durable: boolean;
493
+ persistence: string;
494
+ /** Real row count in the backing table — null when nothing is on disk. */
495
+ persistedRows: number | null;
496
+ fallbackFrom?: string;
497
+ };
490
498
  } | null>;
491
499
  /**
492
500
  * Store to hierarchical memory with tier.
@@ -506,6 +514,23 @@ export declare function bridgeHealthCheck(dbPath?: string): Promise<{
506
514
  * temporal fields are stored in metadata, and supersede is reported as
507
515
  * unsupported (no public update API) rather than silently dropped.
508
516
  */
517
+ /**
518
+ * Describe which store `agentdb_hierarchical-*` actually landed on and
519
+ * whether its writes survive the process (#2887).
520
+ *
521
+ * agentdb removed its `HierarchicalMemory` export at 3.0.0-alpha.17, so the
522
+ * @claude-flow/memory TieredMemoryStore fallback is the live path. It is only
523
+ * durable when it was handed a SQLite connection — callers must not report a
524
+ * volatile write as a success.
525
+ */
526
+ export declare function describeHierarchicalStore(hm: any, fallback: {
527
+ reason: string;
528
+ } | null): {
529
+ controller: string;
530
+ fallbackFrom?: string;
531
+ durable: boolean;
532
+ persistence: string;
533
+ };
509
534
  export declare function bridgeHierarchicalStore(params: {
510
535
  key: string;
511
536
  value: string;
@@ -2392,7 +2392,18 @@ export async function bridgeHealthCheck(dbPath) {
2392
2392
  const s = cache.stats();
2393
2393
  cacheStats = { size: s.size ?? 0, hits: s.hits ?? 0, misses: s.misses ?? 0 };
2394
2394
  }
2395
- return { available: true, controllers, attestationCount, cacheStats };
2395
+ // #2887: cacheStats alone let a "successful" hierarchical-store coexist
2396
+ // with an empty store. Report the backing table's real row count so the
2397
+ // inconsistency is visible instead of inferred.
2398
+ let hierarchicalMemory;
2399
+ const hm = registry.get('hierarchicalMemory');
2400
+ if (hm) {
2401
+ hierarchicalMemory = {
2402
+ ...describeHierarchicalStore(hm, getHierarchicalFallback(registry)),
2403
+ persistedRows: typeof hm.countPersisted === 'function' ? hm.countPersisted() : null,
2404
+ };
2405
+ }
2406
+ return { available: true, controllers, attestationCount, cacheStats, hierarchicalMemory };
2396
2407
  }
2397
2408
  catch {
2398
2409
  return null;
@@ -2417,6 +2428,34 @@ export async function bridgeHealthCheck(dbPath) {
2417
2428
  * temporal fields are stored in metadata, and supersede is reported as
2418
2429
  * unsupported (no public update API) rather than silently dropped.
2419
2430
  */
2431
+ /**
2432
+ * Describe which store `agentdb_hierarchical-*` actually landed on and
2433
+ * whether its writes survive the process (#2887).
2434
+ *
2435
+ * agentdb removed its `HierarchicalMemory` export at 3.0.0-alpha.17, so the
2436
+ * @claude-flow/memory TieredMemoryStore fallback is the live path. It is only
2437
+ * durable when it was handed a SQLite connection — callers must not report a
2438
+ * volatile write as a success.
2439
+ */
2440
+ export function describeHierarchicalStore(hm, fallback) {
2441
+ // Only the tiered fallback exposes isDurable(); the native controller
2442
+ // (should agentdb reintroduce it) writes to agentdb's own tables.
2443
+ if (typeof hm?.isDurable !== 'function') {
2444
+ return { controller: 'hierarchicalMemory', durable: true, persistence: 'agentdb' };
2445
+ }
2446
+ return {
2447
+ controller: 'tieredMemoryStore',
2448
+ fallbackFrom: fallback?.reason ?? 'hierarchicalMemory',
2449
+ durable: hm.isDurable() === true,
2450
+ persistence: typeof hm.getPersistence === 'function' ? hm.getPersistence() : 'unknown',
2451
+ };
2452
+ }
2453
+ /** Read `getHierarchicalFallback()` off a registry that may predate it. */
2454
+ function getHierarchicalFallback(registry) {
2455
+ return typeof registry?.getHierarchicalFallback === 'function'
2456
+ ? registry.getHierarchicalFallback()
2457
+ : null;
2458
+ }
2420
2459
  export async function bridgeHierarchicalStore(params) {
2421
2460
  const registry = await getRegistry();
2422
2461
  if (!registry)
@@ -2439,7 +2478,10 @@ export async function bridgeHierarchicalStore(params) {
2439
2478
  metadata,
2440
2479
  tags: [params.key],
2441
2480
  });
2442
- const result = { success: true, id, key: params.key, tier };
2481
+ const result = {
2482
+ success: true, id, key: params.key, tier,
2483
+ controller: 'hierarchicalMemory', durable: true, persistence: 'agentdb',
2484
+ };
2443
2485
  if (params.supersedes) {
2444
2486
  // No public update/invalidate API on agentdb HierarchicalMemory —
2445
2487
  // surface the limitation honestly instead of silently dropping it.
@@ -2448,17 +2490,28 @@ export async function bridgeHierarchicalStore(params) {
2448
2490
  }
2449
2491
  return result;
2450
2492
  }
2451
- // TieredMemoryStore fallback (temporal-aware) / legacy stub
2493
+ // TieredMemoryStore fallback (temporal-aware) / legacy stub.
2494
+ // agentdb dropped its HierarchicalMemory export at 3.0.0-alpha.17, so
2495
+ // this is the live path — and it is only trustworthy when the store is
2496
+ // backed by SQLite. A volatile write must NOT report success (#2887).
2452
2497
  const storeResult = hm.store(params.key, params.value, tier, {
2453
2498
  validFrom: params.validFrom,
2454
2499
  validUntil: params.validUntil,
2455
2500
  supersedes: params.supersedes,
2456
2501
  });
2457
- if (storeResult && typeof storeResult === 'object') {
2458
- return { success: true, ...storeResult };
2502
+ const info = describeHierarchicalStore(hm, getHierarchicalFallback(registry));
2503
+ const base = {
2504
+ ...info,
2505
+ ...(storeResult && typeof storeResult === 'object' ? storeResult : { key: params.key, tier }),
2506
+ };
2507
+ if (!info.durable) {
2508
+ return {
2509
+ ...base,
2510
+ success: false,
2511
+ error: `hierarchical-store is running on the ${info.persistence} tiered fallback (${info.fallbackFrom}); the entry is NOT persisted to disk and will be lost when this process exits. Use memory_store for durable key-value persistence.`,
2512
+ };
2459
2513
  }
2460
- // Legacy stub (returns void) — temporal options were ignored
2461
- return { success: true, key: params.key, tier };
2514
+ return { success: true, ...base };
2462
2515
  }
2463
2516
  catch (e) {
2464
2517
  return { success: false, error: e.message };
@@ -2523,7 +2576,7 @@ export async function bridgeHierarchicalRecall(params) {
2523
2576
  const filtered = params.tier
2524
2577
  ? results.filter((r) => r.tier === params.tier)
2525
2578
  : results;
2526
- return { results: filtered, controller: 'hierarchicalMemory' };
2579
+ return { results: filtered, ...describeHierarchicalStore(hm, getHierarchicalFallback(registry)) };
2527
2580
  }
2528
2581
  catch (e) {
2529
2582
  return { results: [], error: e.message };
@@ -547,9 +547,15 @@ export declare function deleteEntry(options: {
547
547
  }>;
548
548
  /**
549
549
  * Advisory O_EXCL lock scoped to a single memory.db file (`<dbPath>.lock`),
550
- * same stale-takeover pattern as services/global-ai-budget.ts. Only
551
- * meaningful between callers that opt into it (purgeNamespace does); it
552
- * cannot coordinate against writers that don't call this helper.
550
+ * same stale-takeover pattern as services/global-ai-budget.ts.
551
+ *
552
+ * #2878: sql.js persists by rewriting the whole database image, so every
553
+ * `load → mutate → export → write` sequence is a read-modify-write that a
554
+ * concurrent writer can clobber — both callers report success and the loser's
555
+ * rows vanish, with `PRAGMA integrity_check` still clean. Every such sequence
556
+ * in this module now runs inside this lock. It is advisory, so it still
557
+ * cannot coordinate against a writer that bypasses this module entirely
558
+ * (a native WAL connection — see hasNativeWalSidecars).
553
559
  */
554
560
  export declare function withMemoryDbLock<T>(dbPath: string, fn: () => Promise<T> | T): Promise<T>;
555
561
  export declare function purgeNamespace(options: {