@claude-flow/cli 3.25.6 → 3.26.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 (104) 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 +5 -4
  4. package/.claude/helpers/hook-handler.cjs +129 -0
  5. package/.claude/helpers/statusline.cjs +771 -505
  6. package/.claude/proven-config.json +42 -0
  7. package/catalog-manifest.json +12 -0
  8. package/dist/src/commands/advisor.d.ts +15 -0
  9. package/dist/src/commands/advisor.js +94 -0
  10. package/dist/src/commands/doctor.js +77 -3
  11. package/dist/src/commands/funnel.d.ts +13 -0
  12. package/dist/src/commands/funnel.js +103 -0
  13. package/dist/src/commands/hooks.js +112 -46
  14. package/dist/src/commands/index.js +10 -0
  15. package/dist/src/commands/init.js +44 -0
  16. package/dist/src/commands/proxy.d.ts +21 -0
  17. package/dist/src/commands/proxy.js +310 -0
  18. package/dist/src/commands/security.js +29 -0
  19. package/dist/src/commands/settings.d.ts +19 -0
  20. package/dist/src/commands/settings.js +180 -0
  21. package/dist/src/commands/version.d.ts +42 -0
  22. package/dist/src/commands/version.js +106 -0
  23. package/dist/src/funnel/advisor-tip.d.ts +58 -0
  24. package/dist/src/funnel/advisor-tip.js +92 -0
  25. package/dist/src/funnel/attribution.d.ts +37 -0
  26. package/dist/src/funnel/attribution.js +101 -0
  27. package/dist/src/funnel/consent.d.ts +22 -0
  28. package/dist/src/funnel/consent.js +55 -0
  29. package/dist/src/funnel/credit-errors.d.ts +31 -0
  30. package/dist/src/funnel/credit-errors.js +88 -0
  31. package/dist/src/funnel/credit-notifier.d.ts +44 -0
  32. package/dist/src/funnel/credit-notifier.js +74 -0
  33. package/dist/src/funnel/disclosure.d.ts +46 -0
  34. package/dist/src/funnel/disclosure.js +97 -0
  35. package/dist/src/funnel/enrollment.d.ts +36 -0
  36. package/dist/src/funnel/enrollment.js +64 -0
  37. package/dist/src/funnel/environment.d.ts +17 -0
  38. package/dist/src/funnel/environment.js +39 -0
  39. package/dist/src/funnel/event-transport.d.ts +51 -0
  40. package/dist/src/funnel/event-transport.js +199 -0
  41. package/dist/src/funnel/events.d.ts +42 -0
  42. package/dist/src/funnel/events.js +150 -0
  43. package/dist/src/funnel/index.d.ts +20 -0
  44. package/dist/src/funnel/index.js +20 -0
  45. package/dist/src/funnel/insights.d.ts +49 -0
  46. package/dist/src/funnel/insights.js +120 -0
  47. package/dist/src/funnel/local-signals.d.ts +15 -0
  48. package/dist/src/funnel/local-signals.js +68 -0
  49. package/dist/src/funnel/message-transport.d.ts +51 -0
  50. package/dist/src/funnel/message-transport.js +149 -0
  51. package/dist/src/funnel/messages.d.ts +55 -0
  52. package/dist/src/funnel/messages.js +160 -0
  53. package/dist/src/funnel/power-saver-notifier.d.ts +44 -0
  54. package/dist/src/funnel/power-saver-notifier.js +92 -0
  55. package/dist/src/funnel/precedence.d.ts +16 -0
  56. package/dist/src/funnel/precedence.js +85 -0
  57. package/dist/src/funnel/promo.d.ts +41 -0
  58. package/dist/src/funnel/promo.js +144 -0
  59. package/dist/src/funnel/rate-limit-notifier.d.ts +55 -0
  60. package/dist/src/funnel/rate-limit-notifier.js +102 -0
  61. package/dist/src/funnel/rotation.d.ts +19 -0
  62. package/dist/src/funnel/rotation.js +70 -0
  63. package/dist/src/funnel/state.d.ts +13 -0
  64. package/dist/src/funnel/state.js +52 -0
  65. package/dist/src/funnel/toggle-cooldown.d.ts +17 -0
  66. package/dist/src/funnel/toggle-cooldown.js +32 -0
  67. package/dist/src/funnel/types.d.ts +86 -0
  68. package/dist/src/funnel/types.js +26 -0
  69. package/dist/src/init/executor.js +9 -3
  70. package/dist/src/init/helper-refresh.d.ts +33 -2
  71. package/dist/src/init/helper-refresh.js +102 -12
  72. package/dist/src/init/helpers-generator.js +52 -0
  73. package/dist/src/init/statusline-generator.js +282 -116
  74. package/dist/src/services/daemon-autostart.js +28 -5
  75. package/dist/src/services/fable-harness.d.ts +39 -0
  76. package/dist/src/services/fable-harness.js +41 -0
  77. package/package.json +13 -4
  78. package/dist/src/ruvector/lattice-wasm.d.ts +0 -14
  79. package/dist/src/ruvector/lattice-wasm.js +0 -144
  80. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +0 -178
  81. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +0 -1
  82. package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +0 -5
  83. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +0 -43
  84. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783604774864_ozbujc_prompt.log +0 -19
  85. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783604774864_ozbujc_result.log +0 -108
  86. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783605513587_ulvmpb_prompt.log +0 -19
  87. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783605513587_ulvmpb_result.log +0 -209
  88. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783606368867_ahysui_prompt.log +0 -19
  89. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783606368867_ahysui_result.log +0 -192
  90. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783604894861_v6n3ut_prompt.log +0 -14
  91. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783604894861_v6n3ut_result.log +0 -66
  92. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783605934532_9h8ikb_prompt.log +0 -14
  93. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783605934532_9h8ikb_result.log +0 -68
  94. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783605134860_9jssz9_prompt.log +0 -14
  95. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783605134860_9jssz9_result.log +0 -69
  96. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783606516743_zftbaa_prompt.log +0 -14
  97. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +0 -7
  98. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +0 -11
  99. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +0 -16
  100. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +0 -83
  101. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +0 -67
  102. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +0 -191
  103. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +0 -68
  104. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
@@ -0,0 +1,42 @@
1
+ {
2
+ "adoptedAt": 1783692950735,
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
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generation": 1,
4
+ "generatedAt": "2026-07-13T16:09:11.080Z",
5
+ "gitSha": "2d42012e",
6
+ "catalog": {
7
+ "agents": 164,
8
+ "tools": 387,
9
+ "skills": 34
10
+ },
11
+ "benchmark": null
12
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `ruflo advisor` — Fable co-pilot advisor tip in the statusline insight
3
+ * ticker (ADR-316).
4
+ *
5
+ * Client-side only — unlike `proxy.ts`'s sponsored-downtime/power-saver/
6
+ * training-share domains, this feature has NO proxy-config.toml mirror,
7
+ * because it never talks to the meta-proxy Rust binary. It calls
8
+ * `claude -p` directly via services/fable-harness.ts (ADR-172's existing
9
+ * cost-disciplined harness) and caches the result for the insight ticker
10
+ * (funnel/insights.ts) to read synchronously.
11
+ */
12
+ import type { Command } from '../types.js';
13
+ export declare const advisorCommand: Command;
14
+ export default advisorCommand;
15
+ //# sourceMappingURL=advisor.d.ts.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `ruflo advisor` — Fable co-pilot advisor tip in the statusline insight
3
+ * ticker (ADR-316).
4
+ *
5
+ * Client-side only — unlike `proxy.ts`'s sponsored-downtime/power-saver/
6
+ * training-share domains, this feature has NO proxy-config.toml mirror,
7
+ * because it never talks to the meta-proxy Rust binary. It calls
8
+ * `claude -p` directly via services/fable-harness.ts (ADR-172's existing
9
+ * cost-disciplined harness) and caches the result for the insight ticker
10
+ * (funnel/insights.ts) to read synchronously.
11
+ */
12
+ import { output } from '../output.js';
13
+ import { hasConsent, recordConsent, revokeConsent } from '../funnel/consent.js';
14
+ import { recordFunnelEvent } from '../funnel/events.js';
15
+ import { getInstalledCliVersion } from '../init/helper-refresh.js';
16
+ import { readAdvisorTip, ADVISOR_DEFAULT_BUDGET_USD } from '../funnel/advisor-tip.js';
17
+ const ADVISOR_DISCLOSURE = [
18
+ 'Enabling the co-pilot advisor tip.',
19
+ '',
20
+ 'At most once per day, ruflo will send a small STRUCTURAL snapshot of your',
21
+ 'session — security scan status, swarm/agent state, git uncommitted-file',
22
+ 'COUNT — never raw prompts, file contents, or commands — to a headless',
23
+ 'Fable model (via `claude -p`) and cache one short, actionable tip for the',
24
+ 'statusline insight ticker.',
25
+ '',
26
+ `This is a real, metered API call (~$${ADVISOR_DEFAULT_BUDGET_USD.toFixed(2)} budget cap per`,
27
+ 'refresh, at most once/day). Disable anytime: ruflo advisor disable',
28
+ ].join('\n');
29
+ const enableSub = {
30
+ name: 'enable',
31
+ description: 'Opt into the co-pilot advisor tip (ADR-316)',
32
+ options: [
33
+ { name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
34
+ ],
35
+ action: async (ctx) => {
36
+ if (hasConsent('advisor-tips')) {
37
+ output.writeln('Advisor tip is already enabled.');
38
+ return { success: true, data: { alreadyEnabled: true } };
39
+ }
40
+ output.writeln(ADVISOR_DISCLOSURE);
41
+ output.writeln('');
42
+ if (!ctx.flags.yes) {
43
+ output.writeln('Re-run with --yes to confirm: ruflo advisor enable --yes');
44
+ return { success: true, data: { confirmed: false } };
45
+ }
46
+ recordConsent('advisor-tips', true, 'advisor-enable');
47
+ recordFunnelEvent('advisor_tip_enabled', 'statusline', getInstalledCliVersion());
48
+ output.printSuccess('Advisor tip enabled.');
49
+ output.writeln('It refreshes at most once/day, in the background, on session-restore.');
50
+ output.writeln('Disable anytime: ruflo advisor disable');
51
+ return { success: true, data: { confirmed: true } };
52
+ },
53
+ };
54
+ const disableSub = {
55
+ name: 'disable',
56
+ description: 'Revoke advisor-tip consent and stop generating new tips',
57
+ action: async () => {
58
+ revokeConsent('advisor-tips', 'advisor-disable');
59
+ recordFunnelEvent('advisor_tip_disabled', 'statusline', getInstalledCliVersion());
60
+ output.printSuccess('Advisor tip disabled.');
61
+ return { success: true };
62
+ },
63
+ };
64
+ const statusSub = {
65
+ name: 'status',
66
+ description: 'Show advisor-tip consent state and the current cached tip',
67
+ action: async () => {
68
+ const consented = hasConsent('advisor-tips');
69
+ output.writeln(`Advisor tip consent: ${consented ? 'granted' : 'not granted'}`);
70
+ const tip = consented ? readAdvisorTip() : null;
71
+ if (tip) {
72
+ output.writeln(`Current tip: ${tip.headline}`);
73
+ if (tip.detail)
74
+ output.writeln(` ${tip.detail}`);
75
+ }
76
+ else if (consented) {
77
+ output.writeln('No cached tip yet (refreshes at most once/day on session-restore).');
78
+ }
79
+ return { success: true, data: { consented, tip } };
80
+ },
81
+ };
82
+ export const advisorCommand = {
83
+ name: 'advisor',
84
+ description: 'Fable co-pilot advisor tip in the statusline insight ticker (ADR-316)',
85
+ subcommands: [enableSub, disableSub, statusSub],
86
+ examples: [
87
+ { command: 'ruflo advisor enable --yes', description: 'Opt into the co-pilot advisor tip' },
88
+ { command: 'ruflo advisor status', description: 'Show consent state + current cached tip' },
89
+ { command: 'ruflo advisor disable', description: 'Revoke consent and stop generating tips' },
90
+ ],
91
+ action: statusSub.action,
92
+ };
93
+ export default advisorCommand;
94
+ //# sourceMappingURL=advisor.js.map
@@ -135,8 +135,10 @@ async function checkConfigFile() {
135
135
  * the broken commands).
136
136
  */
137
137
  async function checkStaleSettingsNpx() {
138
- // Same regex pattern the executor migration uses — kept in sync.
139
- const BROKEN_RE = /npx\s+(?:--?\S+\s+)*@?claude-flow\/cli@latest\s+hooks\s+(?:statusline|\S+)/;
138
+ // Same regex pattern the executor migration uses — kept in sync. Flag-list
139
+ // repetition bounded at 10 (CodeQL js/redos — unbounded `*` here is
140
+ // exponential-backtracking-prone on a crafted settings.json).
141
+ const BROKEN_RE = /npx\s+(?:--?\S+\s+){0,10}@?claude-flow\/cli@latest\s+hooks\s+(?:statusline|\S+)/;
140
142
  // Look in both project-local and home-dir settings.
141
143
  const candidates = [
142
144
  join(process.cwd(), '.claude', 'settings.json'),
@@ -711,6 +713,74 @@ async function checkVersionFreshness() {
711
713
  * - plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md
712
714
  * - plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md (iter 53)
713
715
  */
716
+ /**
717
+ * ADR-305 — funnel state audit. Reports the effective enabled/disabled
718
+ * state and, critically for enterprise audit verification, WHICH
719
+ * precedence source decided it (env > enterprise > user > default).
720
+ * Informational: both states are `pass`; only a resolver failure warns.
721
+ */
722
+ async function checkFunnel() {
723
+ try {
724
+ const { resolveFunnelEnabled, getDisclosure } = await import('../funnel/index.js');
725
+ const decision = resolveFunnelEnabled();
726
+ const disclosure = getDisclosure();
727
+ return {
728
+ name: 'Funnel (ADR-305)',
729
+ status: 'pass',
730
+ message: `${decision.enabled ? 'enabled' : 'disabled'} (decided by: ${decision.decidedBy}; disclosure: ${disclosure.state})`,
731
+ };
732
+ }
733
+ catch (err) {
734
+ return {
735
+ name: 'Funnel (ADR-305)',
736
+ status: 'warn',
737
+ message: `state unreadable: ${err instanceof Error ? err.message : String(err)}`,
738
+ fix: 'ruflo funnel status',
739
+ };
740
+ }
741
+ }
742
+ /** Meta LLM Proxy — sponsored-downtime health (ADR-313). */
743
+ async function checkProxy() {
744
+ try {
745
+ const { funnelStateDir, hasConsent, readRateLimitStatus, lastRecordedEvent } = await import('../funnel/index.js');
746
+ const dir = funnelStateDir();
747
+ const installed = existsSync(join(dir, 'proxy-token'));
748
+ const consented = hasConsent('sponsored-downtime');
749
+ const rateLimited = readRateLimitStatus();
750
+ const lastExhausted = lastRecordedEvent('sponsor_capacity_exhausted');
751
+ if (!installed) {
752
+ return {
753
+ name: 'Meta LLM Proxy (ADR-313)',
754
+ status: 'warn',
755
+ message: 'not installed — no proxy-token found; sponsored-downtime capacity is unavailable',
756
+ fix: 'See cognitum-one/meta-proxy (private) for install instructions',
757
+ };
758
+ }
759
+ const parts = [
760
+ `sponsored consent: ${consented ? 'granted' : 'not granted'}`,
761
+ `rate-limit flag: ${rateLimited.limited ? `set (since ${rateLimited.since})` : 'not set'}`,
762
+ ];
763
+ if (lastExhausted)
764
+ parts.push(`last capacity-exhausted: ${lastExhausted}`);
765
+ if (rateLimited.limited && !consented) {
766
+ return {
767
+ name: 'Meta LLM Proxy (ADR-313)',
768
+ status: 'warn',
769
+ message: `${parts.join('; ')} — flagged rate-limited but sponsored capacity isn't enabled`,
770
+ fix: 'ruflo proxy sponsor-enable --yes',
771
+ };
772
+ }
773
+ return { name: 'Meta LLM Proxy (ADR-313)', status: 'pass', message: parts.join('; ') };
774
+ }
775
+ catch (err) {
776
+ return {
777
+ name: 'Meta LLM Proxy (ADR-313)',
778
+ status: 'warn',
779
+ message: `state unreadable: ${err instanceof Error ? err.message : String(err)}`,
780
+ fix: 'ruflo proxy sponsor-status',
781
+ };
782
+ }
783
+ }
714
784
  async function checkMetaharnessIntegration() {
715
785
  // Locate plugins dir.
716
786
  //
@@ -1087,7 +1157,7 @@ export const doctorCommand = {
1087
1157
  {
1088
1158
  name: 'component',
1089
1159
  short: 'c',
1090
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, metaharness)',
1160
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, metaharness)',
1091
1161
  type: 'string'
1092
1162
  },
1093
1163
  {
@@ -1137,6 +1207,8 @@ export const doctorCommand = {
1137
1207
  checkFederationBreaker, // ADR-097 Phase 4
1138
1208
  checkMetaharness, // ADR-150 — MetaHarness upstream package
1139
1209
  checkMetaharnessIntegration, // iter 45 — ruflo-side integration layer
1210
+ checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
1211
+ checkProxy, // ADR-313 — Meta LLM Proxy sponsored-downtime health
1140
1212
  ];
1141
1213
  const componentMap = {
1142
1214
  'version': checkVersionFreshness,
@@ -1161,6 +1233,8 @@ export const doctorCommand = {
1161
1233
  'federation': checkFederationBreaker, // ADR-097 Phase 4
1162
1234
  'metaharness': checkMetaharness, // ADR-150 — upstream package
1163
1235
  'metaharness-integration': checkMetaharnessIntegration, // iter 45 — ruflo-side
1236
+ 'funnel': checkFunnel, // ADR-305
1237
+ 'proxy': checkProxy, // ADR-313
1164
1238
  };
1165
1239
  let checksToRun = allChecks;
1166
1240
  if (component && componentMap[component]) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `ruflo funnel` — user control surface for the Cognitum lifecycle funnel
3
+ * (ADR-301/305/309).
4
+ *
5
+ * funnel status effective state and which precedence source decided it
6
+ * funnel disable user-level opt-out (all surfaces) + delete funnel data
7
+ * funnel enable re-enable at the user tier (cannot override env/enterprise)
8
+ * funnel id print the pseudonymous funnel ID, if one exists
9
+ */
10
+ import type { Command } from '../types.js';
11
+ export declare const funnelCommand: Command;
12
+ export default funnelCommand;
13
+ //# sourceMappingURL=funnel.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `ruflo funnel` — user control surface for the Cognitum lifecycle funnel
3
+ * (ADR-301/305/309).
4
+ *
5
+ * funnel status effective state and which precedence source decided it
6
+ * funnel disable user-level opt-out (all surfaces) + delete funnel data
7
+ * funnel enable re-enable at the user tier (cannot override env/enterprise)
8
+ * funnel id print the pseudonymous funnel ID, if one exists
9
+ */
10
+ import { output } from '../output.js';
11
+ import { deleteFunnelData, funnelStateDir, getDisclosure, getFunnelId, readConsents, recordDisclosureDeclined, recordDisclosureReenabled, resolveFunnelEnabled, } from '../funnel/index.js';
12
+ import { readStateJson, writeStateJson } from '../funnel/state.js';
13
+ function setUserConfigEnabled(enabled) {
14
+ const cfg = readStateJson('funnel.json') ?? {};
15
+ cfg.enabled = enabled;
16
+ writeStateJson('funnel.json', cfg);
17
+ }
18
+ const statusSub = {
19
+ name: 'status',
20
+ description: 'Show effective funnel state and which source decided it',
21
+ options: [
22
+ { name: 'json', description: 'Output as JSON', type: 'boolean', default: false },
23
+ ],
24
+ action: async (ctx) => {
25
+ const decision = resolveFunnelEnabled();
26
+ const disclosure = getDisclosure();
27
+ const consents = readConsents();
28
+ const data = {
29
+ enabled: decision.enabled,
30
+ decidedBy: decision.decidedBy,
31
+ disclosure: disclosure.state,
32
+ stateDir: funnelStateDir(),
33
+ consents,
34
+ };
35
+ if (ctx.flags.json) {
36
+ output.printJson(data);
37
+ }
38
+ else {
39
+ output.writeln(`Funnel: ${decision.enabled ? 'enabled' : 'disabled'} (decided by: ${decision.decidedBy})`);
40
+ output.writeln(`Disclosure: ${disclosure.state}`);
41
+ output.writeln(`State dir: ${funnelStateDir()}`);
42
+ const domains = Object.keys(consents);
43
+ output.writeln(domains.length
44
+ ? `Consents: ${domains.map((d) => `${d}=${consents[d]?.granted ? 'granted' : 'declined'}`).join(', ')}`
45
+ : 'Consents: none recorded');
46
+ }
47
+ return { success: true, data };
48
+ },
49
+ };
50
+ const disableSub = {
51
+ name: 'disable',
52
+ description: 'Disable all funnel surfaces (user-level, persists across projects)',
53
+ action: async () => {
54
+ setUserConfigEnabled(false);
55
+ recordDisclosureDeclined();
56
+ deleteFunnelData(); // opt-out deletes the pseudonymous ID + local event queue
57
+ output.printSuccess('Funnel disabled. All promotional surfaces are off; local funnel data deleted.');
58
+ return { success: true };
59
+ },
60
+ };
61
+ const enableSub = {
62
+ name: 'enable',
63
+ description: 'Re-enable funnel surfaces at the user tier (env/enterprise disables still win)',
64
+ action: async () => {
65
+ setUserConfigEnabled(true);
66
+ recordDisclosureReenabled();
67
+ const decision = resolveFunnelEnabled();
68
+ if (decision.enabled) {
69
+ output.printSuccess('Funnel enabled.');
70
+ }
71
+ else {
72
+ // A lower-precedence source never overrides a higher-precedence disable.
73
+ output.printWarning(`User preference recorded, but the funnel stays disabled by a higher-precedence source: ${decision.decidedBy}`);
74
+ }
75
+ return { success: true, data: decision };
76
+ },
77
+ };
78
+ const idSub = {
79
+ name: 'id',
80
+ description: 'Print the pseudonymous funnel ID (exists only with telemetry consent)',
81
+ action: async () => {
82
+ const id = getFunnelId();
83
+ if (id) {
84
+ output.writeln(id);
85
+ }
86
+ else {
87
+ output.writeln('No funnel ID (telemetry consent not granted, or funnel data deleted).');
88
+ }
89
+ return { success: true, data: { id } };
90
+ },
91
+ };
92
+ export const funnelCommand = {
93
+ name: 'funnel',
94
+ description: 'Control the Cognitum lifecycle funnel surfaces (tips, enrollment, notices)',
95
+ subcommands: [statusSub, disableSub, enableSub, idSub],
96
+ examples: [
97
+ { command: 'ruflo funnel status', description: 'Effective state + deciding source' },
98
+ { command: 'ruflo funnel disable', description: 'Turn off every funnel surface' },
99
+ ],
100
+ action: statusSub.action,
101
+ };
102
+ export default funnelCommand;
103
+ //# sourceMappingURL=funnel.js.map
@@ -8,6 +8,7 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import { storeCommand } from './transfer-store.js';
9
9
  import { existsSync, readFileSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
11
12
  /**
12
13
  * #1686 — `?? 0` only defaults null/undefined; NaN slips through and
13
14
  * surfaces as `"NaN"` (or earlier crashed `.toFixed`) in the metrics
@@ -3412,52 +3413,11 @@ const statuslineCommand = {
3412
3413
  const dddProgress = Math.min(100, Math.floor((domainsCompleted / totalDomains) * 100));
3413
3414
  return { domainsCompleted, totalDomains, dddProgress, patternsLearned: learning.patterns, sessionsCompleted: learning.sessions };
3414
3415
  }
3415
- // Get security status
3416
- function getSecurityStatus() {
3417
- const scanResultsPath = path.join(process.cwd(), '.claude', 'security-scans');
3418
- let cvesFixed = 0;
3419
- const totalCves = 3;
3420
- if (fs.existsSync(scanResultsPath)) {
3421
- try {
3422
- const scans = fs.readdirSync(scanResultsPath).filter((f) => f.endsWith('.json'));
3423
- cvesFixed = Math.min(totalCves, scans.length);
3424
- }
3425
- catch {
3426
- // Ignore
3427
- }
3428
- }
3429
- const auditPath = path.join(process.cwd(), '.swarm', 'security');
3430
- if (fs.existsSync(auditPath)) {
3431
- try {
3432
- const audits = fs.readdirSync(auditPath).filter((f) => f.includes('audit'));
3433
- cvesFixed = Math.min(totalCves, Math.max(cvesFixed, audits.length));
3434
- }
3435
- catch {
3436
- // Ignore
3437
- }
3438
- }
3439
- const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
3440
- return { status, cvesFixed, totalCves };
3441
- }
3442
- // Get swarm status
3443
- function getSwarmStatus() {
3444
- let activeAgents = 0;
3445
- let coordinationActive = false;
3446
- const maxAgents = 15;
3447
- const isWindows = process.platform === 'win32';
3448
- try {
3449
- const psCmd = isWindows
3450
- ? 'tasklist /FI "IMAGENAME eq node.exe" /NH 2>NUL | find /c /v "" 2>NUL || echo 0'
3451
- : 'ps aux 2>/dev/null | grep -c agentic-flow || echo "0"';
3452
- const ps = execSync(psCmd, { encoding: 'utf-8', timeout: 3000 });
3453
- activeAgents = Math.max(0, parseInt(ps.trim()) - 1);
3454
- coordinationActive = activeAgents > 0;
3455
- }
3456
- catch {
3457
- // ps/tasklist unavailable or timed out — report zero
3458
- }
3459
- return { activeAgents, maxAgents, coordinationActive };
3460
- }
3416
+ // Security/swarm status — shared with the advisor-tip refresh (ADR-316)
3417
+ // via funnel/local-signals.ts, a single source of truth so the two call
3418
+ // sites can never silently drift on what these signals mean.
3419
+ const getSecurityStatus = sharedGetSecurityStatus;
3420
+ const getSwarmStatus = sharedGetSwarmStatus;
3461
3421
  // Get system metrics
3462
3422
  function getSystemMetrics() {
3463
3423
  let memoryMB = 0;
@@ -3554,12 +3514,30 @@ const statuslineCommand = {
3554
3514
  const swarm = getSwarmStatus();
3555
3515
  const system = getSystemMetrics();
3556
3516
  const user = getUserInfo();
3517
+ const getGitUncommittedCount = sharedGetGitUncommittedCount;
3518
+ // Funnel promo row (ADR-301). The statusline is spawned with piped stdio
3519
+ // by an interactive host, so interactivity is asserted here; all other
3520
+ // gates (RUFLO_FUNNEL, enterprise policy, config, CI, disclosure,
3521
+ // rotation ratio) are enforced inside getFunnelPromo. Never allowed to
3522
+ // break the statusline.
3523
+ let promo = null;
3524
+ try {
3525
+ const { getFunnelPromo } = await import('../funnel/index.js');
3526
+ promo = getFunnelPromo({
3527
+ interactive: true,
3528
+ localInsights: { security, swarm, gitUncommittedCount: getGitUncommittedCount() },
3529
+ });
3530
+ }
3531
+ catch {
3532
+ promo = null;
3533
+ }
3557
3534
  const statusData = {
3558
3535
  user,
3559
3536
  v3Progress: progress,
3560
3537
  security,
3561
3538
  swarm,
3562
3539
  system,
3540
+ promo,
3563
3541
  timestamp: new Date().toISOString()
3564
3542
  };
3565
3543
  // JSON output
@@ -4418,6 +4396,90 @@ const notifyCommand = {
4418
4396
  return { success: true, data: { timestamp, level, message } };
4419
4397
  }
4420
4398
  };
4399
+ // Refresh-funnel subcommand — the fix for "promo doesn't load right away".
4400
+ //
4401
+ // refreshRemoteMessages() (funnel/message-transport.ts) is fire-and-forget
4402
+ // by design so the STATUSLINE's own per-render invocation never blocks on
4403
+ // a network call. But the statusline is spawned as a short-lived subprocess
4404
+ // per render (execSync from statusline-generator.ts) — a fire-and-forget
4405
+ // promise kicked off there has no "later" to run in; the process exits
4406
+ // before the HTTPS fetch can complete, so the local message cache never
4407
+ // actually gets written and the promo row never appears (confirmed live:
4408
+ // two consecutive cold-cache statusline renders, 5s apart, both returned
4409
+ // promo:null and the cache file was never created).
4410
+ //
4411
+ // This command exists to be invoked from a LONGER-LIVED context — the
4412
+ // SessionStart hook (see hook-handler.cjs's 'session-restore' handler,
4413
+ // which spawns this detached so it isn't killed when the hook's own
4414
+ // process exits, and isn't awaited so it doesn't add to the hook's own
4415
+ // timeout budget). One real, properly-awaited refresh attempt here, once
4416
+ // per session, is what actually gives refreshRemoteMessages() a chance to
4417
+ // finish before anything needs the cache.
4418
+ const refreshFunnelCommand = {
4419
+ name: 'refresh-funnel',
4420
+ description: 'Best-effort background refresh of the funnel message cache (internal — see hook-handler.cjs session-restore)',
4421
+ options: [
4422
+ { name: 'quiet', description: 'Suppress output (used when spawned detached from a hook)', type: 'boolean', default: false },
4423
+ ],
4424
+ action: async (ctx) => {
4425
+ try {
4426
+ const { refreshRemoteMessages } = await import('../funnel/index.js');
4427
+ const result = await refreshRemoteMessages();
4428
+ if (!ctx.flags.quiet) {
4429
+ output.writeln(JSON.stringify(result));
4430
+ }
4431
+ return { success: true, data: result };
4432
+ }
4433
+ catch (error) {
4434
+ // Fail silent by design (matches message-transport.ts's own "fail
4435
+ // silent" discipline) — a broken refresh must never surface as a
4436
+ // hook error, only ever as "no promo this session."
4437
+ if (!ctx.flags.quiet) {
4438
+ output.printError(`refresh-funnel failed: ${error instanceof Error ? error.message : String(error)}`);
4439
+ }
4440
+ return { success: true, data: { refreshed: false, skipped: 'error' } };
4441
+ }
4442
+ }
4443
+ };
4444
+ // Refresh-advisor subcommand — ADR-316's co-pilot tip. Mirrors
4445
+ // refreshFunnelCommand exactly: a properly-awaited CLI subcommand meant to
4446
+ // be spawned DETACHED from hook-handler.cjs's session-restore handler, so a
4447
+ // real (potentially multi-second, real-money) `claude -p` call gets a
4448
+ // chance to finish without ever blocking or being awaited by the hook's own
4449
+ // process. refreshAdvisorTipIfStale() itself is the safety net that makes
4450
+ // this cheap to call on every session-restore: it checks consent + a 24h
4451
+ // TTL BEFORE spending anything, so most invocations are a no-op file read.
4452
+ const refreshAdvisorCommand = {
4453
+ name: 'refresh-advisor',
4454
+ description: 'Best-effort background refresh of the co-pilot advisor tip (internal — see hook-handler.cjs session-restore; ADR-316)',
4455
+ options: [
4456
+ { name: 'quiet', description: 'Suppress output (used when spawned detached from a hook)', type: 'boolean', default: false },
4457
+ ],
4458
+ action: async (ctx) => {
4459
+ try {
4460
+ const { refreshAdvisorTipIfStale } = await import('../funnel/advisor-tip.js');
4461
+ const { getSecurityStatus, getSwarmStatus, getGitUncommittedCount } = await import('../funnel/local-signals.js');
4462
+ const result = await refreshAdvisorTipIfStale({
4463
+ security: getSecurityStatus(),
4464
+ swarm: getSwarmStatus(),
4465
+ gitUncommittedCount: getGitUncommittedCount(),
4466
+ });
4467
+ if (!ctx.flags.quiet) {
4468
+ output.writeln(JSON.stringify(result));
4469
+ }
4470
+ return { success: true, data: result };
4471
+ }
4472
+ catch (error) {
4473
+ // Fail silent by design (matches refresh-funnel's own discipline) — a
4474
+ // broken advisor refresh must never surface as a hook error, only
4475
+ // ever as "no tip this window."
4476
+ if (!ctx.flags.quiet) {
4477
+ output.printError(`refresh-advisor failed: ${error instanceof Error ? error.message : String(error)}`);
4478
+ }
4479
+ return { success: true, data: { refreshed: false, reason: 'error' } };
4480
+ }
4481
+ }
4482
+ };
4421
4483
  // Main hooks command
4422
4484
  export const hooksCommand = {
4423
4485
  name: 'hooks',
@@ -4461,6 +4523,10 @@ export const hooksCommand = {
4461
4523
  // Agent Teams integration
4462
4524
  teammateIdleCommand,
4463
4525
  taskCompletedCommand,
4526
+ // Funnel background refresh — see refreshFunnelCommand's own doc comment
4527
+ refreshFunnelCommand,
4528
+ // Advisor co-pilot tip background refresh — ADR-316
4529
+ refreshAdvisorCommand,
4464
4530
  ],
4465
4531
  options: [],
4466
4532
  examples: [
@@ -31,6 +31,7 @@ const commandLoaders = {
31
31
  'hive-mind': () => import('./hive-mind.js'),
32
32
  process: () => import('./process.js'),
33
33
  daemon: () => import('./daemon.js'),
34
+ version: () => import('./version.js'),
34
35
  // V3 Advanced Commands (less frequently used - lazy load)
35
36
  neural: () => import('./neural.js'),
36
37
  security: () => import('./security.js'),
@@ -73,6 +74,15 @@ const commandLoaders = {
73
74
  metaharness: () => import('./metaharness.js'),
74
75
  // Eject (ADR-150 Phase 2) — lift ruflo project into a renamed standalone harness
75
76
  eject: () => import('./eject.js'),
77
+ // Cognitum lifecycle funnel controls (ADR-301/305/309)
78
+ funnel: () => import('./funnel.js'),
79
+ // User-facing preferences wrapper (ADR-311 copy discipline — no "funnel" in
80
+ // the user surface). Forwards to the funnel primitives internally.
81
+ settings: () => import('./settings.js'),
82
+ // Meta LLM Proxy — sponsored downtime capacity (ADR-304/307/313)
83
+ proxy: () => import('./proxy.js'),
84
+ // Fable co-pilot advisor tip in the statusline insight ticker (ADR-316)
85
+ advisor: () => import('./advisor.js'),
76
86
  };
77
87
  // Cache for loaded commands
78
88
  const loadedCommands = new Map();